diff --git a/.gitmodules b/.gitmodules index 773dea971..5daf41c03 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,7 @@ [submodule "extern/filecoin-ffi"] path = extern/filecoin-ffi url = https://github.com/filecoin-project/filecoin-ffi.git +[submodule "extern/supra_seal"] + path = extern/supra_seal + url = https://github.com/magik6k/supra_seal.git + branch = feat/multi-out-paths diff --git a/Makefile b/Makefile index fe2f8dd7f..91caa407d 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,8 @@ SHELL=/usr/bin/env bash GOCC?=go +## FILECOIN-FFI + FFI_PATH:=extern/filecoin-ffi/ FFI_DEPS:=.install-filcrypto FFI_DEPS:=$(addprefix $(FFI_PATH),$(FFI_DEPS)) @@ -22,6 +24,23 @@ BUILD_DEPS+=ffi-version-check .PHONY: ffi-version-check +## SUPRA-FFI + +ifeq ($(shell uname),Linux) +SUPRA_FFI_PATH:=extern/supra_seal/ +SUPRA_FFI_DEPS:=.install-supraseal +SUPRA_FFI_DEPS:=$(addprefix $(SUPRA_FFI_PATH),$(SUPRA_FFI_DEPS)) + +$(SUPRA_FFI_DEPS): build/.supraseal-install ; + +build/.supraseal-install: $(SUPRA_FFI_PATH) + cd $(SUPRA_FFI_PATH) && ./build.sh + @touch $@ + +MODULES+=$(SUPRA_FFI_PATH) +CLEAN+=build/.supraseal-install +endif + $(MODULES): build/.update-modules ; # dummy file that marks the last time modules were updated build/.update-modules: @@ -30,6 +49,12 @@ build/.update-modules: # end git modules +## CUDA Library Path +CUDA_PATH := $(shell dirname $$(dirname $$(which nvcc))) +CUDA_LIB_PATH := $(CUDA_PATH)/lib64 +LIBRARY_PATH ?= $(CUDA_LIB_PATH) +export LIBRARY_PATH + ## MAIN BINARIES CLEAN+=build/.update-modules @@ -41,7 +66,7 @@ deps: $(BUILD_DEPS) curio: $(BUILD_DEPS) rm -f curio - GOAMD64=v3 $(GOCC) build $(GOFLAGS) -o curio -ldflags " -s -w \ + GOAMD64=v3 CGO_LDFLAGS_ALLOW=$(CGO_LDFLAGS_ALLOW) $(GOCC) build $(GOFLAGS) -o curio -ldflags " -s -w \ -X github.com/filecoin-project/curio/build.IsOpencl=$(FFI_USE_OPENCL) \ -X github.com/filecoin-project/curio/build.CurrentCommit=+git_`git log -1 --format=%h_%cI`" \ ./cmd/curio @@ -54,6 +79,31 @@ sptool: $(BUILD_DEPS) .PHONY: sptool BINS+=sptool +ifeq ($(shell uname),Linux) + +batchdep: build/.supraseal-install +batchdep: $(BUILD_DEPS) +,PHONY: batchdep + +batch: GOFLAGS+=-tags=supraseal +batch: CGO_LDFLAGS_ALLOW='.*' +batch: batchdep build +.PHONY: batch + +batch-calibnet: GOFLAGS+=-tags=calibnet,supraseal +batch-calibnet: CGO_LDFLAGS_ALLOW='.*' +batch-calibnet: batchdep build +.PHONY: batch-calibnet + +else +batch: + @echo "Batch target is only available on Linux systems" + @exit 1 + +batch-calibnet: + @echo "Batch-calibnet target is only available on Linux systems" + @exit 1 +endif calibnet: GOFLAGS+=-tags=calibnet calibnet: build diff --git a/cmd/curio/calc.go b/cmd/curio/calc.go new file mode 100644 index 000000000..538cdad69 --- /dev/null +++ b/cmd/curio/calc.go @@ -0,0 +1,169 @@ +package main + +import ( + "fmt" + + "github.com/fatih/color" + "github.com/urfave/cli/v2" + + "github.com/filecoin-project/curio/tasks/sealsupra" +) + +var calcCmd = &cli.Command{ + Name: "calc", + Usage: "Math Utils", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "actor", + }, + }, + Subcommands: []*cli.Command{ + calcBatchCpuCmd, + calcSuprasealConfigCmd, + }, +} + +var calcBatchCpuCmd = &cli.Command{ + Name: "batch-cpu", + Usage: "Analyze and display the layout of batch sealer threads", + Description: `Analyze and display the layout of batch sealer threads on your CPU. + +It provides detailed information about CPU utilization for batch sealing operations, including core allocation, thread +distribution for different batch sizes.`, + Flags: []cli.Flag{ + &cli.BoolFlag{Name: "dual-hashers", Value: true}, + }, + Action: func(cctx *cli.Context) error { + info, err := sealsupra.GetSystemInfo() + if err != nil { + return err + } + + fmt.Println("Basic CPU Information") + fmt.Println("") + fmt.Printf("Processor count: %d\n", info.ProcessorCount) + fmt.Printf("Core count: %d\n", info.CoreCount) + fmt.Printf("Thread count: %d\n", info.CoreCount*info.ThreadsPerCore) + fmt.Printf("Threads per core: %d\n", info.ThreadsPerCore) + fmt.Printf("Cores per L3 cache (CCX): %d\n", info.CoresPerL3) + fmt.Printf("L3 cache count (CCX count): %d\n", info.CoreCount/info.CoresPerL3) + + ccxFreeCores := info.CoresPerL3 - 1 // one core per ccx goes to the coordinator + ccxFreeThreads := ccxFreeCores * info.ThreadsPerCore + fmt.Printf("Hasher Threads per CCX: %d\n", ccxFreeThreads) + + sectorsPerThread := 1 + if cctx.Bool("dual-hashers") { + sectorsPerThread = 2 + } + + sectorsPerCCX := ccxFreeThreads * sectorsPerThread + fmt.Printf("Sectors per CCX: %d\n", sectorsPerCCX) + + fmt.Println("---------") + + printForBatchSize := func(batchSize int) { + fmt.Printf("Batch Size: %s sectors\n", color.CyanString("%d", batchSize)) + fmt.Println() + + config, err := sealsupra.GenerateSupraSealConfig(*info, cctx.Bool("dual-hashers"), batchSize, nil) + if err != nil { + fmt.Printf("Error generating config: %s\n", err) + return + } + + fmt.Printf("Required Threads: %d\n", config.RequiredThreads) + fmt.Printf("Required CCX: %d\n", config.RequiredCCX) + fmt.Printf("Required Cores: %d hasher (+4 minimum for non-hashers)\n", config.RequiredCores) + + enoughCores := config.RequiredCores <= info.CoreCount + if enoughCores { + fmt.Printf("Enough cores available for hashers %s\n", color.GreenString("✔")) + } else { + fmt.Printf("Not enough cores available for hashers %s\n", color.RedString("✘")) + return + } + + fmt.Printf("Non-hasher cores: %d\n", info.CoreCount-config.RequiredCores) + + if config.P2WrRdOverlap { + color.Yellow("! P2 writer will share a core with P2 reader, performance may be impacted") + } + if config.P2HsP1WrOverlap { + color.Yellow("! P2 hasher will share a core with P1 writer, performance may be impacted") + } + if config.P2HcP2RdOverlap { + color.Yellow("! P2 hasher_cpu will share a core with P2 reader, performance may be impacted") + } + + fmt.Println() + fmt.Printf("pc1 writer: %d\n", config.Topology.PC1Writer) + fmt.Printf("pc1 reader: %d\n", config.Topology.PC1Reader) + fmt.Printf("pc1 orchestrator: %d\n", config.Topology.PC1Orchestrator) + fmt.Println() + fmt.Printf("pc2 reader: %d\n", config.Topology.PC2Reader) + fmt.Printf("pc2 hasher: %d\n", config.Topology.PC2Hasher) + fmt.Printf("pc2 hasher_cpu: %d\n", config.Topology.PC2HasherCPU) + fmt.Printf("pc2 writer: %d\n", config.Topology.PC2Writer) + fmt.Printf("pc2 writer_cores: %d\n", config.Topology.PC2WriterCores) + fmt.Println() + fmt.Printf("c1 reader: %d\n", config.Topology.C1Reader) + fmt.Println() + + fmt.Printf("Unoccupied Cores: %d\n\n", config.UnoccupiedCores) + + fmt.Println("{") + fmt.Printf(" sectors = %d;\n", batchSize) + fmt.Println(" coordinators = (") + for i, coord := range config.Topology.SectorConfigs[0].Coordinators { + fmt.Printf(" { core = %d;\n hashers = %d; }", coord.Core, coord.Hashers) + if i < len(config.Topology.SectorConfigs[0].Coordinators)-1 { + fmt.Println(",") + } else { + fmt.Println() + } + } + fmt.Println(" )") + fmt.Println("}") + + fmt.Println("---------") + } + + printForBatchSize(16) + printForBatchSize(32) + printForBatchSize(64) + printForBatchSize(128) + + return nil + }, +} + +var calcSuprasealConfigCmd = &cli.Command{ + Name: "supraseal-config", + Usage: "Generate a supra_seal configuration", + Description: `Generate a supra_seal configuration for a given batch size. + +This command outputs a configuration expected by SupraSeal. Main purpose of this command is for debugging and testing. +The config can be used directly with SupraSeal binaries to test it without involving Curio.`, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "dual-hashers", + Value: true, + Usage: "Zen3 and later supports two sectors per thread, set to false for older CPUs", + }, + &cli.IntFlag{ + Name: "batch-size", + Aliases: []string{"b"}, + Required: true, + }, + }, + Action: func(cctx *cli.Context) error { + cstr, err := sealsupra.GenerateSupraSealConfigString(cctx.Bool("dual-hashers"), cctx.Int("batch-size"), nil) + if err != nil { + return err + } + + fmt.Println(cstr) + return nil + }, +} diff --git a/cmd/curio/main.go b/cmd/curio/main.go index 0a0a55ffa..38f8c8604 100644 --- a/cmd/curio/main.go +++ b/cmd/curio/main.go @@ -67,6 +67,7 @@ func main() { marketCmd, fetchParamCmd, ffiCmd, + calcCmd, } jaeger := tracing.SetupJaegerTracing("curio") diff --git a/cmd/curio/pipeline.go b/cmd/curio/pipeline.go index 6854e52c9..c1738b4d4 100644 --- a/cmd/curio/pipeline.go +++ b/cmd/curio/pipeline.go @@ -9,6 +9,8 @@ import ( "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/builtin" + miner12 "github.com/filecoin-project/go-state-types/builtin/v12/miner" "github.com/filecoin-project/curio/cmd/curio/guidedsetup" "github.com/filecoin-project/curio/deps" @@ -61,6 +63,12 @@ var sealStartCmd = &cli.Command{ Name: "layers", Usage: "list of layers to be interpreted (atop defaults). Default: base", }, + &cli.IntFlag{ + Name: "duration-days", + Aliases: []string{"d"}, + Usage: "How long to commit sectors for", + DefaultText: "1278 (3.5 years)", + }, }, Action: func(cctx *cli.Context) error { if !cctx.Bool("now") { @@ -118,9 +126,23 @@ var sealStartCmd = &cli.Command{ return xerrors.Errorf("getting seal proof type: %w", err) } + var userDuration *int64 + if cctx.IsSet("duration-days") { + days := cctx.Int("duration-days") + userDuration = new(int64) + *userDuration = int64(days) * builtin.EpochsInDay + + if miner12.MaxSectorExpirationExtension < *userDuration { + return xerrors.Errorf("duration exceeds max allowed: %d > %d", *userDuration, miner12.MaxSectorExpirationExtension) + } + if miner12.MinSectorExpiration > *userDuration { + return xerrors.Errorf("duration is too short: %d < %d", *userDuration, miner12.MinSectorExpiration) + } + } + num, err := seal.AllocateSectorNumbers(ctx, dep.Chain, dep.DB, act, cctx.Int("count"), func(tx *harmonydb.Tx, numbers []abi.SectorNumber) (bool, error) { for _, n := range numbers { - _, err := tx.Exec("insert into sectors_sdr_pipeline (sp_id, sector_number, reg_seal_proof) values ($1, $2, $3)", mid, n, spt) + _, err := tx.Exec("insert into sectors_sdr_pipeline (sp_id, sector_number, reg_seal_proof, user_sector_duration_epochs) values ($1, $2, $3, $4)", mid, n, spt, userDuration) if err != nil { return false, xerrors.Errorf("inserting into sectors_sdr_pipeline: %w", err) } diff --git a/cmd/curio/rpc/rpc.go b/cmd/curio/rpc/rpc.go index 984d8006f..882003c94 100644 --- a/cmd/curio/rpc/rpc.go +++ b/cmd/curio/rpc/rpc.go @@ -30,6 +30,7 @@ import ( "github.com/filecoin-project/curio/api/client" "github.com/filecoin-project/curio/build" "github.com/filecoin-project/curio/deps" + "github.com/filecoin-project/curio/lib/metrics" "github.com/filecoin-project/curio/lib/paths" "github.com/filecoin-project/curio/lib/repo" "github.com/filecoin-project/curio/web" @@ -37,7 +38,7 @@ import ( lapi "github.com/filecoin-project/lotus/api" cliutil "github.com/filecoin-project/lotus/cli/util" "github.com/filecoin-project/lotus/lib/rpcenc" - "github.com/filecoin-project/lotus/metrics" + lotusmetrics "github.com/filecoin-project/lotus/metrics" "github.com/filecoin-project/lotus/metrics/proxy" "github.com/filecoin-project/lotus/storage/pipeline/piece" "github.com/filecoin-project/lotus/storage/sealer/fsutil" @@ -71,6 +72,7 @@ func CurioHandler( mux.Handle("/rpc/v0", rpcServer) mux.Handle("/rpc/streams/v0/push/{uuid}", readerHandler) mux.PathPrefix("/remote").HandlerFunc(remote) + mux.Handle("/debug/metrics", metrics.Exporter()) mux.PathPrefix("/").Handler(http.DefaultServeMux) // pprof if !permissioned { @@ -283,7 +285,7 @@ func ListenAndServe(ctx context.Context, dependencies *deps.Deps, shutdownChan c permissioned), ReadHeaderTimeout: time.Minute * 3, BaseContext: func(listener net.Listener) context.Context { - ctx, _ := tag.New(context.Background(), tag.Upsert(metrics.APIInterface, "lotus-worker")) + ctx, _ := tag.New(context.Background(), tag.Upsert(lotusmetrics.APIInterface, "curio")) return ctx }, Addr: dependencies.ListenAddr, diff --git a/cmd/curio/run.go b/cmd/curio/run.go index e6eb99275..9a359cddf 100644 --- a/cmd/curio/run.go +++ b/cmd/curio/run.go @@ -106,14 +106,7 @@ var runCmd = &cli.Command{ ctxclose() }() } - // Register all metric views - /* - if err := view.Register( - metrics.MinerNodeViews..., - ); err != nil { - log.Fatalf("Cannot register the view: %v", err) - } - */ + // Set the metric to one so it is published to the exporter stats.Record(ctx, metrics.LotusInfo.M(1)) diff --git a/cmd/curio/tasks/tasks.go b/cmd/curio/tasks/tasks.go index a33189373..2b0fd0891 100644 --- a/cmd/curio/tasks/tasks.go +++ b/cmd/curio/tasks/tasks.go @@ -13,6 +13,7 @@ import ( "github.com/samber/lo" "github.com/snadrus/must" "golang.org/x/exp/maps" + "golang.org/x/xerrors" "github.com/filecoin-project/go-address" @@ -28,11 +29,13 @@ import ( "github.com/filecoin-project/curio/lib/ffi" "github.com/filecoin-project/curio/lib/multictladdr" "github.com/filecoin-project/curio/lib/paths" + "github.com/filecoin-project/curio/lib/slotmgr" "github.com/filecoin-project/curio/tasks/gc" "github.com/filecoin-project/curio/tasks/message" "github.com/filecoin-project/curio/tasks/metadata" piece2 "github.com/filecoin-project/curio/tasks/piece" "github.com/filecoin-project/curio/tasks/seal" + "github.com/filecoin-project/curio/tasks/sealsupra" "github.com/filecoin-project/curio/tasks/snap" window2 "github.com/filecoin-project/curio/tasks/window" "github.com/filecoin-project/curio/tasks/winning" @@ -83,6 +86,7 @@ func StartTasks(ctx context.Context, dependencies *deps.Deps) (*harmonytask.Task lstor := dependencies.LocalStore si := dependencies.Si bstore := dependencies.Bstore + machine := dependencies.ListenAddr var activeTasks []harmonytask.TaskInterface sender, sendTask := message.NewSender(full, full, db) @@ -169,12 +173,13 @@ func StartTasks(ctx context.Context, dependencies *deps.Deps) (*harmonytask.Task cfg.Subsystems.EnablePoRepProof || cfg.Subsystems.EnableMoveStorage || cfg.Subsystems.EnableSendCommitMsg || + cfg.Subsystems.EnableBatchSeal || cfg.Subsystems.EnableUpdateEncode || cfg.Subsystems.EnableUpdateProve || cfg.Subsystems.EnableUpdateSubmit if hasAnySealingTask { - sealingTasks, err := addSealingTasks(ctx, hasAnySealingTask, db, full, sender, as, cfg, slrLazy, asyncParams, si, stor, bstore) + sealingTasks, err := addSealingTasks(ctx, hasAnySealingTask, db, full, sender, as, cfg, slrLazy, asyncParams, si, stor, bstore, machine) if err != nil { return nil, err } @@ -222,7 +227,7 @@ func addSealingTasks( ctx context.Context, hasAnySealingTask bool, db *harmonydb.DB, full api.Chain, sender *message.Sender, as *multictladdr.MultiAddressSelector, cfg *config.CurioConfig, slrLazy *lazy.Lazy[*ffi.SealCalls], asyncParams func() func() (bool, error), si paths.SectorIndex, stor *paths.Remote, - bstore curiochain.CurioBlockstore) ([]harmonytask.TaskInterface, error) { + bstore curiochain.CurioBlockstore, machineHostPort string) ([]harmonytask.TaskInterface, error) { var activeTasks []harmonytask.TaskInterface // Sealing / Snap @@ -235,18 +240,43 @@ func addSealingTasks( slr = must.One(slrLazy.Val()) } + var slotMgr *slotmgr.SlotMgr + var addFinalize bool + // NOTE: Tasks with the LEAST priority are at the top + if cfg.Subsystems.EnableBatchSeal { + slotMgr = slotmgr.NewSlotMgr() + + batchSealTask, err := sealsupra.NewSupraSeal( + cfg.Seal.BatchSealSectorSize, + cfg.Seal.BatchSealBatchSize, + cfg.Seal.BatchSealPipelines, + !cfg.Seal.SingleHasherPerThread, + cfg.Seal.LayerNVMEDevices, + machineHostPort, slotMgr, db, full, stor, si) + if err != nil { + return nil, xerrors.Errorf("setting up batch sealer: %w", err) + } + activeTasks = append(activeTasks, batchSealTask) + addFinalize = true + } + if cfg.Subsystems.EnableSealSDR { - sdrTask := seal.NewSDRTask(full, db, sp, slr, cfg.Subsystems.SealSDRMaxTasks) + sdrTask := seal.NewSDRTask(full, db, sp, slr, cfg.Subsystems.SealSDRMaxTasks, cfg.Subsystems.SealSDRMinTasks) activeTasks = append(activeTasks, sdrTask) } if cfg.Subsystems.EnableSealSDRTrees { treeDTask := seal.NewTreeDTask(sp, db, slr, cfg.Subsystems.SealSDRTreesMaxTasks) treeRCTask := seal.NewTreeRCTask(sp, db, slr, cfg.Subsystems.SealSDRTreesMaxTasks) synthTask := seal.NewSyntheticProofTask(sp, db, slr, cfg.Subsystems.SyntheticPoRepMaxTasks) - finalizeTask := seal.NewFinalizeTask(cfg.Subsystems.FinalizeMaxTasks, sp, slr, db) - activeTasks = append(activeTasks, treeDTask, treeRCTask, synthTask, finalizeTask) + activeTasks = append(activeTasks, treeDTask, synthTask, treeRCTask) + addFinalize = true + } + if addFinalize { + finalizeTask := seal.NewFinalizeTask(cfg.Subsystems.FinalizeMaxTasks, sp, slr, db, slotMgr) + activeTasks = append(activeTasks, finalizeTask) } + if cfg.Subsystems.EnableSendPrecommitMsg { precommitTask := seal.NewSubmitPrecommitTask(sp, db, full, sender, as, cfg.Fees.MaxPreCommitGasFee, cfg.Fees.CollateralFromMinerBalance, cfg.Fees.DisableCollateralFallback) activeTasks = append(activeTasks, precommitTask) diff --git a/deps/apiinfo.go b/deps/apiinfo.go index e96938788..6638940dd 100644 --- a/deps/apiinfo.go +++ b/deps/apiinfo.go @@ -14,7 +14,6 @@ import ( "github.com/filecoin-project/curio/api" - lapi "github.com/filecoin-project/lotus/api" cliutil "github.com/filecoin-project/lotus/cli/util" "github.com/filecoin-project/lotus/lib/retry" ) @@ -71,13 +70,6 @@ func GetFullNodeAPIV1Curio(ctx *cli.Context, ainfoCfg []string) (api.Chain, json var v1API api.ChainStruct FullNodeProxy(fullNodes, &v1API) - v, err := v1API.Version(ctx.Context) - if err != nil { - return nil, nil, err - } - if !v.APIVersion.EqMajorMinor(lapi.FullAPIVersion1) { - return nil, nil, xerrors.Errorf("Remote API version didn't match (expected %s, remote %s)", lapi.FullAPIVersion1, v.APIVersion) - } return &v1API, finalCloser, nil } diff --git a/deps/config/doc_gen.go b/deps/config/doc_gen.go index 4646f66eb..fb67ef5cf 100644 --- a/deps/config/doc_gen.go +++ b/deps/config/doc_gen.go @@ -139,6 +139,12 @@ alerts will be triggered for the wallet`, Comment: ``, }, + { + Name: "Seal", + Type: "CurioSealConfig", + + Comment: ``, + }, { Name: "Apis", Type: "ApisConfig", @@ -374,6 +380,47 @@ Note that setting this value lower may result in less efficient gas use - more m to prove each deadline, resulting in more total gas use (but each message will have lower gas limit)`, }, }, + "CurioSealConfig": { + { + Name: "BatchSealSectorSize", + Type: "string", + + Comment: `BatchSealSectorSize Allows setting the sector size supported by the batch seal task. +Can be any value as long as it is "32GiB".`, + }, + { + Name: "BatchSealBatchSize", + Type: "int", + + Comment: `Number of sectors in a seal batch. Depends on hardware and supraseal configuration.`, + }, + { + Name: "BatchSealPipelines", + Type: "int", + + Comment: `Number of parallel pipelines. Can be 1 or 2. Depends on available raw block storage`, + }, + { + Name: "SingleHasherPerThread", + Type: "bool", + + Comment: `SingleHasherPerThread is a compatibility flag for older CPUs. Zen3 and later supports two sectors per thread. +Set to false for older CPUs (Zen 2 and before).`, + }, + { + Name: "LayerNVMEDevices", + Type: "[]string", + + Comment: `LayerNVMEDevices is a list of pcie device addresses that should be used for SDR layer storage. +The required storage is 11 * BatchSealBatchSize * BatchSealSectorSize * BatchSealPipelines +Total Read IOPS for optimal performance should be 10M+. +The devices MUST be NVMe devices, not used for anything else. Any data on the devices will be lost! + +It's recommend to define these settings in a per-machine layer, as the devices are machine-specific. + +Example: ["0000:01:00.0", "0000:01:00.1"]`, + }, + }, "CurioSubsystemsConfig": { { Name: "EnableWindowPost", @@ -444,6 +491,17 @@ In lotus-miner this was run as part of PreCommit1.`, Comment: `The maximum amount of SDR tasks that can run simultaneously. Note that the maximum number of tasks will also be bounded by resources available on the machine.`, + }, + { + Name: "SealSDRMinTasks", + Type: "int", + + Comment: `The maximum amount of SDR tasks that need to be queued before the system will start accepting new tasks. +The main purpose of this setting is to allow for enough tasks to accumulate for batch sealing. When batch sealing +nodes are present in the cluster, this value should be set to batch_size+1 to allow for the batch sealing node to +fill up the batch. +This setting can also be used to give priority to other nodes in the cluster by setting this value to a higher +value on the nodes which should have less priority.`, }, { Name: "EnableSealSDRTrees", @@ -632,6 +690,12 @@ cache data held on disk after the completion of TreeRC task to 11GiB.`, Comment: `The maximum amount of SyntheticPoRep tasks that can run simultaneously. Note that the maximum number of tasks will also be bounded by resources available on the machine.`, }, + { + Name: "EnableBatchSeal", + Type: "bool", + + Comment: `Batch Seal`, + }, }, "Duration time.Duration": { { diff --git a/deps/config/types.go b/deps/config/types.go index db1605d19..4e344ce56 100644 --- a/deps/config/types.go +++ b/deps/config/types.go @@ -45,6 +45,11 @@ func DefaultCurioConfig() *CurioConfig { PartitionCheckTimeout: Duration(20 * time.Minute), SingleCheckTimeout: Duration(10 * time.Minute), }, + Seal: CurioSealConfig{ + BatchSealPipelines: 2, + BatchSealBatchSize: 32, + BatchSealSectorSize: "32GiB", + }, Ingest: CurioIngestConfig{ MaxQueueDealSector: 8, // default to 8 sectors open(or in process of opening) for deals MaxQueueSDR: 8, // default to 8 (will cause backpressure even if deal sectors are 0) @@ -73,6 +78,7 @@ type CurioConfig struct { Addresses []CurioAddresses Proving CurioProvingConfig Ingest CurioIngestConfig + Seal CurioSealConfig Apis ApisConfig Alerting CurioAlertingConfig } @@ -127,6 +133,14 @@ type CurioSubsystemsConfig struct { // also be bounded by resources available on the machine. SealSDRMaxTasks int + // The maximum amount of SDR tasks that need to be queued before the system will start accepting new tasks. + // The main purpose of this setting is to allow for enough tasks to accumulate for batch sealing. When batch sealing + // nodes are present in the cluster, this value should be set to batch_size+1 to allow for the batch sealing node to + // fill up the batch. + // This setting can also be used to give priority to other nodes in the cluster by setting this value to a higher + // value on the nodes which should have less priority. + SealSDRMinTasks int + // EnableSealSDRTrees enables the SDR pipeline tree-building task to run. // This task handles encoding of unsealed data into last sdr layer and building // of TreeR, TreeC and TreeD. @@ -249,6 +263,9 @@ type CurioSubsystemsConfig struct { // The maximum amount of SyntheticPoRep tasks that can run simultaneously. Note that the maximum number of tasks will // also be bounded by resources available on the machine. SyntheticPoRepMaxTasks int + + // Batch Seal + EnableBatchSeal bool } type CurioFees struct { DefaultMaxFee types.FIL @@ -446,6 +463,32 @@ type CurioAlertingConfig struct { SlackWebhook SlackWebhookConfig } +type CurioSealConfig struct { + // BatchSealSectorSize Allows setting the sector size supported by the batch seal task. + // Can be any value as long as it is "32GiB". + BatchSealSectorSize string + + // Number of sectors in a seal batch. Depends on hardware and supraseal configuration. + BatchSealBatchSize int + + // Number of parallel pipelines. Can be 1 or 2. Depends on available raw block storage + BatchSealPipelines int + + // SingleHasherPerThread is a compatibility flag for older CPUs. Zen3 and later supports two sectors per thread. + // Set to false for older CPUs (Zen 2 and before). + SingleHasherPerThread bool + + // LayerNVMEDevices is a list of pcie device addresses that should be used for SDR layer storage. + // The required storage is 11 * BatchSealBatchSize * BatchSealSectorSize * BatchSealPipelines + // Total Read IOPS for optimal performance should be 10M+. + // The devices MUST be NVMe devices, not used for anything else. Any data on the devices will be lost! + // + // It's recommend to define these settings in a per-machine layer, as the devices are machine-specific. + // + // Example: ["0000:01:00.0", "0000:01:00.1"] + LayerNVMEDevices []string +} + type PagerDutyConfig struct { // Enable is a flag to enable or disable the PagerDuty integration. Enable bool diff --git a/documentation/en/SUMMARY.md b/documentation/en/SUMMARY.md index 810398ba3..e50d28459 100644 --- a/documentation/en/SUMMARY.md +++ b/documentation/en/SUMMARY.md @@ -16,6 +16,7 @@ * [Default Curio Configuration](configuration/default-curio-configuration.md) * [Enabling market](enabling-market.md) * [Snap Deals](snap-deals.md) +* [Batch Sealing with SupraSeal](supraseal.md) * [Scaling Curio cluster](scaling-curio-cluster.md) * [Curio GUI](curio-gui.md) * [Garbage Collection](garbage-collection.md) diff --git a/documentation/en/configuration/default-curio-configuration.md b/documentation/en/configuration/default-curio-configuration.md index cb84b3422..fff7b7123 100644 --- a/documentation/en/configuration/default-curio-configuration.md +++ b/documentation/en/configuration/default-curio-configuration.md @@ -62,6 +62,16 @@ description: The default curio configuration # type: int #SealSDRMaxTasks = 0 + # The maximum amount of SDR tasks that need to be queued before the system will start accepting new tasks. + # The main purpose of this setting is to allow for enough tasks to accumulate for batch sealing. When batch sealing + # nodes are present in the cluster, this value should be set to batch_size+1 to allow for the batch sealing node to + # fill up the batch. + # This setting can also be used to give priority to other nodes in the cluster by setting this value to a higher + # value on the nodes which should have less priority. + # + # type: int + #SealSDRMinTasks = 0 + # EnableSealSDRTrees enables the SDR pipeline tree-building task to run. # This task handles encoding of unsealed data into last sdr layer and building # of TreeR, TreeC and TreeD. @@ -228,6 +238,11 @@ description: The default curio configuration # type: int #SyntheticPoRepMaxTasks = 0 + # Batch Seal + # + # type: bool + #EnableBatchSeal = false + [Fees] # type: types.FIL @@ -436,6 +451,30 @@ description: The default curio configuration #DoSnap = false +[Seal] + # BatchSealSectorSize Allows setting the sector size supported by the batch seal task. + # Can be any value as long as it is "32GiB". + # + # type: string + #BatchSealSectorSize = "32GiB" + + # Number of sectors in a seal batch. Depends on hardware and supraseal configuration. + # + # type: int + #BatchSealBatchSize = 32 + + # Number of parallel pipelines. Can be 1 or 2. Depends on available raw block storage + # + # type: int + #BatchSealPipelines = 2 + + # SingleHasherPerThread is a compatibility flag for older CPUs. Zen3 and later supports two sectors per thread. + # Set to false for older CPUs (Zen 2 and before). + # + # type: bool + #SingleHasherPerThread = false + + [Apis] # RPC Secret for the storage subsystem. # If integrating with lotus-miner this must match the value from diff --git a/documentation/en/curio-cli/curio.md b/documentation/en/curio-cli/curio.md index 4ae9bb992..f1d7e720e 100644 --- a/documentation/en/curio-cli/curio.md +++ b/documentation/en/curio-cli/curio.md @@ -19,6 +19,7 @@ COMMANDS: seal Manage the sealing pipeline market fetch-params Fetch proving parameters + calc Math Utils help, h Shows a list of commands or help for one command GLOBAL OPTIONS: @@ -517,6 +518,7 @@ OPTIONS: --count value Number of sectors to start (default: 1) --synthetic Use synthetic PoRep (default: false) --layers value [ --layers value ] list of layers to be interpreted (atop defaults). Default: base + --duration-days value, -d value How long to commit sectors for (default: 1278 (3.5 years)) --help, -h show help ``` @@ -575,3 +577,60 @@ USAGE: OPTIONS: --help, -h show help ``` + +## curio calc +``` +NAME: + curio calc - Math Utils + +USAGE: + curio calc command [command options] [arguments...] + +COMMANDS: + batch-cpu Analyze and display the layout of batch sealer threads + supraseal-config Generate a supra_seal configuration + help, h Shows a list of commands or help for one command + +OPTIONS: + --actor value + --help, -h show help +``` + +### curio calc batch-cpu +``` +NAME: + curio calc batch-cpu - Analyze and display the layout of batch sealer threads + +USAGE: + curio calc batch-cpu [command options] [arguments...] + +DESCRIPTION: + Analyze and display the layout of batch sealer threads on your CPU. + + It provides detailed information about CPU utilization for batch sealing operations, including core allocation, thread + distribution for different batch sizes. + +OPTIONS: + --dual-hashers (default: true) + --help, -h show help +``` + +### curio calc supraseal-config +``` +NAME: + curio calc supraseal-config - Generate a supra_seal configuration + +USAGE: + curio calc supraseal-config [command options] [arguments...] + +DESCRIPTION: + Generate a supra_seal configuration for a given batch size. + + This command outputs a configuration expected by SupraSeal. Main purpose of this command is for debugging and testing. + The config can be used directly with SupraSeal binaries to test it without involving Curio. + +OPTIONS: + --dual-hashers Zen3 and later supports two sectors per thread, set to false for older CPUs (default: true) + --batch-size value, -b value (default: 0) + --help, -h show help +``` diff --git a/documentation/en/supraseal.md b/documentation/en/supraseal.md new file mode 100644 index 000000000..4478674ca --- /dev/null +++ b/documentation/en/supraseal.md @@ -0,0 +1,377 @@ +--- +description: >- + This page explains how to setup supraseal batch sealer in Curio +--- + + +# SupraSeal Batch Sealing + +> **Disclaimer:** SupraSeal batch sealing is currently in **BETA**. Use with caution and expect potential issues or changes in future versions. Currently some additional manual system configuration is required. + +SupraSeal is an optimized batch sealing implementation for Filecoin that allows sealing multiple sectors in parallel. It can significantly improve sealing throughput compared to sealing sectors individually. + +## Key Features + +- Seals multiple sectors (up to 128) in a single batch + - Up to 16x better core utilisation efficiency +- Optimized to utilize CPU and GPU resources efficiently +- Uses raw NVMe devices for layer storage instead of RAM + +## Requirements + +- CPU with at least 4 cores per CCX (AMD) or equivalent +- NVMe drives with high IOPS (10-20M total IOPS recommended) +- GPU for PC2 phase (NVIDIA RTX 3090 or better recommended) +- 1GB hugepages configured (minimum 36 pages) +- Ubuntu 22.04 or compatible Linux distribution (gcc-11 required, doesn't need to be system-wide) +- At least 256GB RAM, ALL MEMORY CHANNELS POPULATED + - Without **all** memory channels populated sealing **performance will suffer drastically** +- NUMA-Per-Socket (NPS) set to 1 + +## Storage Recommendations +You need 2 sets of NVMe drives: + +1. Drives for layers: +* Total 10-20M IOPS +* Capacity for 11 x 32G x batchSize x pipelines +* Raw unformatted block devices (SPDK will take them over) +* Each drive should be able to sustain ~2GiB/s of writes + * This requirement isn't understood well yet, it's possible that lower write rates are fine. More testing is needed. +2. Drives for P2 output: +* With a filesystem +* Fast with sufficient capacity (~70G x batchSize x pipelines) +* Can be remote storage if fast enough (~500MiB/s/GPU) + +## Hardware Recommendations + +Currently, the community is trying to determine the best hardware configurations for batch sealing. + +Some general observations are: +* Single socket systems will be easier to use at full capacity +* You want a lot of NVMe slots, on PCIe Gen4 platforms with large batch sizes you may use 20-24 3.84TB NVMe drives +* In general you'll want to make sure all memory channels are populated +* You need 4~8 physical cores (not threads) for batch-wide compute, then on each CCX you'll lose 1 core for a "coordinator" + * Each thread computes 2 sectors + * On zen2 and earlier hashers compute only one sector per thread + * Large (many-core) CCX-es are typically better + +## Setup + +### Dependencies + +CUDA 12.x is required, 11.x won't work. + +```bash + +The build process depends on GCC 11.x system-wide or gcc-11/g++-11 installed locally. +* On Arch install https://aur.archlinux.org/packages/gcc11 +* Ubuntu 22.04 has GCC 11.x by default +* On newer Ubuntu install `gcc-11` and `g++-11` packages + + +### Building + +Build the batch-capable Curio binary: +```bash +make batch +``` + +For calibnet +```bash +make batch-calibnet +``` + +NOTE: the build should be run on the target machine. Binaries won't be postable between CPU generations due to different +AVX512 support. + +## Configuration + +* Run `curio calc batch-cpu` on the target machine to determine supported batch sizes for your CPU + +
+Example batch-cpu output + +``` +# EPYC 7313 16-Core Processor + +root@udon:~# ./curio calc batch-cpu +Basic CPU Information + +Processor count: 1 +Core count: 16 +Thread count: 32 +Threads per core: 2 +Cores per L3 cache (CCX): 4 +L3 cache count (CCX count): 4 +Hasher Threads per CCX: 6 +Sectors per CCX: 12 +--------- +Batch Size: 16 sectors + +Required Threads: 8 +Required CCX: 2 +Required Cores: 6 hasher (+4 minimum for non-hashers) +Enough cores available for hashers ✔ +Non-hasher cores: 10 +Enough cores for coordination ✔ + +pc1 writer: 1 +pc1 reader: 2 +pc1 orchestrator: 3 + +pc2 reader: 4 +pc2 hasher: 5 +pc2 hasher_cpu: 6 +pc2 writer: 7 +pc2 writer_cores: 3 + +c1 reader: 7 + +Unoccupied Cores: 0 + +{ + sectors = 16; + coordinators = ( + { core = 10; + hashers = 2; }, + { core = 12; + hashers = 6; } + ) +} +--------- +Batch Size: 32 sectors + +Required Threads: 16 +Required CCX: 3 +Required Cores: 11 hasher (+4 minimum for non-hashers) +Enough cores available for hashers ✔ +Non-hasher cores: 5 +Enough cores for coordination ✔ +! P2 hasher will share a core with P1 writer, performance may be impacted +! P2 hasher_cpu will share a core with P2 reader, performance may be impacted + +pc1 writer: 1 +pc1 reader: 2 +pc1 orchestrator: 3 + +pc2 reader: 0 +pc2 hasher: 1 +pc2 hasher_cpu: 0 +pc2 writer: 4 +pc2 writer_cores: 1 + +c1 reader: 0 + +Unoccupied Cores: 0 + +{ + sectors = 32; + coordinators = ( + { core = 5; + hashers = 4; }, + { core = 8; + hashers = 6; }, + { core = 12; + hashers = 6; } + ) +} +--------- +Batch Size: 64 sectors + +Required Threads: 32 +Required CCX: 6 +Required Cores: 22 hasher (+4 minimum for non-hashers) +Not enough cores available for hashers ✘ +Batch Size: 128 sectors + +Required Threads: 64 +Required CCX: 11 +Required Cores: 43 hasher (+4 minimum for non-hashers) +Not enough cores available for hashers ✘ + +``` + +
+ +* Create a new layer configuration for the batch sealer, e.g. batch-machine1: + +```toml +[Subsystems] +EnableBatchSeal = true + +[Seal] +LayerNVMEDevices = [ + "0000:88:00.0", + "0000:86:00.0", + # Add PCIe addresses for all NVMe devices to use +] + +# Set to your desired batch size (what the batch-cpu command says your CPU supports AND what you have nvme space for) +BatchSealBatchSize = 32 + +# pipelines can be either 1 or 2; 2 pipelines double storage requirements but in correctly balanced systems makes +# layer hashing run 100% of the time, nearly doubling throughput +BatchSealPipelines = 2 + +# Set to true for Zen2 or older CPUs for compatibility +SingleHasherPerThread = false +``` + +### Configure hugepages: + +This can be done by adding the following to `/etc/default/grub`. You need 36 1G hugepages for the batch sealer. + +```bash +GRUB_CMDLINE_LINUX_DEFAULT="hugepages=36 default_hugepagesz=1G hugepagesz=1G" +``` + +Then run `sudo update-grub` and reboot the machine. + +Or at runtime: +```bash +sudo sysctl -w vm.nr_hugepages=36 +``` + +Then check /proc/meminfo to verify the hugepages are available: +```bash +cat /proc/meminfo | grep Huge +``` + +Expect output like: +``` +AnonHugePages: 0 kB +ShmemHugePages: 0 kB +FileHugePages: 0 kB +HugePages_Total: 36 +HugePages_Free: 36 +HugePages_Rsvd: 0 +HugePages_Surp: 0 +Hugepagesize: 1048576 kB +``` + +Check that `HugePages_Free` is equal to 36, the kernel can sometimes use some of the hugepages for other purposes. + +### Setup NVMe devices for SPDK: + +NOTE: This is only needed while batch sealing is in beta, future versions of Curio will handle this automatically. + +```bash +cd extern/supra_seal/deps/spdk-v22.09/ +env NRHUGE=36 ./scripts/setup.sh +``` + +### PC2 output storage + +Attach scratch space storage for PC2 output (batch sealer needs ~70GB per sector in batch - 32GiB for the sealed sector, +and 36GiB for the cache directory with TreeC/TreeR and aux files) + +## Usage +1. Start the Curio node with the batch sealer layer + +```bash +curio run --layers batch-machine1 +``` + +2. Add a batch of CC sectors: +```bash +curio seal start --now --cc --count 32 --actor f01234 --duration-days 365 +``` + +3. Monitor progress - you should see a "Batch..." task running in the [Curio GUI](curio-gui.md) +4. PC1 will take 3.5-5 hours, followed by PC2 on GPU +5. After batch completion, the storage will be released for the next batch + +## Optimizing + +* Balance batch size, CPU cores, and NVMe drives to keep PC1 running constantly +* Ensure sufficient GPU capacity to complete PC2 before next PC1 batch finishes +* Monitor CPU, GPU and NVMe utilization to identify bottlenecks +* Monitor hasher core utilisation + +## Troubleshooting + +### Node doesn't start / isn't visible in the UI +* Ensure hugepages are configured correctly +* Check NVMe device IOPS and capacity + * If spdk setup fails, try to `wipefs -a` the NVMe devices (this will wipe partitions from the devices, be careful!) + +### Performance issues + +You can monitor performance by looking at "hasher" core utilisation in e.g. `htop`. + +To identify hasher cores, call `curio calc supraseal-config --batch-size 128` (with the correct batch size), and look for `coordinators` + +```go +topology: +... +{ + pc1: { + writer = 1; +... + hashers_per_core = 2; + + sector_configs: ( + { + sectors = 128; + coordinators = ( + { core = 59; + hashers = 8; }, + { core = 64; + hashers = 14; }, + { core = 72; + hashers = 14; }, + { core = 80; + hashers = 14; }, + { core = 88; + hashers = 14; } + ) + } + + ) + }, + + pc2: { +... +} + +``` + +In this example, cores 59, 64, 72, 80, and 88 are "coordinators", with two hashers per core, meaning that +* In first group core 59 is a coordinator, cores 60-63 are hashers (4 hasher cores / 8 hasher threads) +* In second group core 64 is a coordinator, cores 65-71 are hashers (7 hasher cores / 14 hasher threads) +* And so on + +Coordinator cores will usually sit at 100% utilisation, hasher threads **SHOULD** sit at 100% utilisation, anything less +indicates a bottleneck in the system, like not enough NVMe IOPS, not enough Memory bandwidth, or incorrect NUMA setup. + +To troubleshoot: +* Read the requirements at the top of this page very carefully +* Benchmark iops with: +```bash +cd extern/supra_seal/deps/spdk-v22.09/ + +# repeat -b with all devices you plan to use with supra_seal +# NOTE: You want to test with ALL devices so that you can see if there are any bottlenecks in the system +./build/examples/perf -b 0000:85:00.0 -b 0000:86:00.0... -q 64 -o 4096 -w randread -t 10 +``` +You want to see output like +``` +======================================================== + Latency(us) +Device Information : IOPS MiB/s Average min max +PCIE (0000:04:00.0) NSID 1 from core 0: 889422.78 3474.31 71.93 10.05 1040.94 +PCIE (0000:41:00.0) NSID 1 from core 0: 890028.08 3476.67 71.88 10.69 1063.32 +PCIE (0000:42:00.0) NSID 1 from core 0: 890035.08 3476.70 71.88 10.66 1001.86 +PCIE (0000:86:00.0) NSID 1 from core 0: 889259.28 3473.67 71.95 10.62 1003.83 +PCIE (0000:87:00.0) NSID 1 from core 0: 889179.58 3473.36 71.95 10.55 993.32 +PCIE (0000:88:00.0) NSID 1 from core 0: 889272.18 3473.72 71.94 10.38 974.63 +PCIE (0000:c1:00.0) NSID 1 from core 0: 889815.08 3475.84 71.90 10.97 1044.70 +PCIE (0000:c2:00.0) NSID 1 from core 0: 889691.08 3475.36 71.91 11.04 1036.57 +PCIE (0000:c3:00.0) NSID 1 from core 0: 890082.78 3476.89 71.88 10.44 1023.32 +======================================================== +Total : 8006785.90 31276.51 71.91 10.05 1063.32 +``` +With ideally >10M IOPS total for all devices. + +* Validate GPU setup if PC2 is slow +* Review logs for any errors during batch processing diff --git a/extern/supra_seal b/extern/supra_seal new file mode 160000 index 000000000..d7591837c --- /dev/null +++ b/extern/supra_seal @@ -0,0 +1 @@ +Subproject commit d7591837c662bb3996218ca1290e915b8012aa9e diff --git a/go.mod b/go.mod index faa144ec9..13ad0d02c 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/filecoin-project/curio go 1.22.3 require ( + contrib.go.opencensus.io/exporter/prometheus v0.4.2 github.com/BurntSushi/toml v1.3.2 github.com/KarpelesLab/reflink v1.0.1 github.com/alecthomas/jsonschema v0.0.0-20200530073317-71f438968921 @@ -56,9 +57,11 @@ require ( github.com/multiformats/go-multiaddr v0.12.4 github.com/open-rpc/meta-schema v0.0.0-20201029221707-1b72ef2ea333 github.com/pkg/errors v0.9.1 + github.com/prometheus/client_golang v1.19.1 github.com/puzpuzpuz/xsync/v2 v2.4.0 github.com/raulk/clock v1.1.0 github.com/samber/lo v1.39.0 + github.com/sirupsen/logrus v1.9.2 github.com/snadrus/must v0.0.0-20240605044437-98cedd57f8eb github.com/stretchr/testify v1.9.0 github.com/urfave/cli/v2 v2.25.5 @@ -77,7 +80,6 @@ require ( ) require ( - contrib.go.opencensus.io/exporter/prometheus v0.4.2 // indirect github.com/GeertJohan/go.incremental v1.0.0 // indirect github.com/GeertJohan/go.rice v1.0.3 // indirect github.com/Gurpartap/async v0.0.0-20180927173644-4f7f499dd9ee // indirect @@ -142,7 +144,7 @@ require ( github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-ole/go-ole v1.2.5 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-openapi/jsonpointer v0.19.3 // indirect github.com/go-openapi/jsonreference v0.19.4 // indirect github.com/go-openapi/spec v0.19.11 // indirect @@ -267,7 +269,6 @@ require ( github.com/pion/webrtc/v3 v3.2.40 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/polydawn/refmt v0.89.0 // indirect - github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect @@ -279,7 +280,6 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shirou/gopsutil v2.18.12+incompatible // indirect - github.com/sirupsen/logrus v1.9.2 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/triplewz/poseidon v0.0.0-20230828015038-79d8165c88ed // indirect diff --git a/go.sum b/go.sum index 985eac5d6..959e30be1 100644 --- a/go.sum +++ b/go.sum @@ -377,8 +377,9 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY= github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= diff --git a/harmony/harmonydb/sql/20231217-sdr-pipeline.sql b/harmony/harmonydb/sql/20231217-sdr-pipeline.sql index d8fc80553..b96318b53 100644 --- a/harmony/harmonydb/sql/20231217-sdr-pipeline.sql +++ b/harmony/harmonydb/sql/20231217-sdr-pipeline.sql @@ -9,6 +9,9 @@ create table sectors_sdr_pipeline ( create_time timestamp not null default current_timestamp, reg_seal_proof int not null, + -- Added in 20240802-sdr-pipeline-user-expiration.sql + -- user_sector_duration_epochs bigint default null, + -- sdr ticket_epoch bigint, ticket_value bytea, diff --git a/harmony/harmonydb/sql/20240701-batch-sector-refs.sql b/harmony/harmonydb/sql/20240701-batch-sector-refs.sql new file mode 100644 index 000000000..fac3ed229 --- /dev/null +++ b/harmony/harmonydb/sql/20240701-batch-sector-refs.sql @@ -0,0 +1,12 @@ +CREATE TABLE batch_sector_refs ( + sp_id BIGINT NOT NULL, + sector_number BIGINT NOT NULL, + + machine_host_and_port TEXT NOT NULL, + pipeline_slot BIGINT NOT NULL, + + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + + PRIMARY KEY (sp_id, sector_number, machine_host_and_port, pipeline_slot), + FOREIGN KEY (sp_id, sector_number) REFERENCES sectors_sdr_pipeline (sp_id, sector_number) +); \ No newline at end of file diff --git a/harmony/harmonydb/sql/20240802-sdr-pipeline-user-expiration.sql b/harmony/harmonydb/sql/20240802-sdr-pipeline-user-expiration.sql new file mode 100644 index 000000000..b3f3dc7c5 --- /dev/null +++ b/harmony/harmonydb/sql/20240802-sdr-pipeline-user-expiration.sql @@ -0,0 +1,2 @@ +ALTER TABLE sectors_sdr_pipeline + ADD COLUMN user_sector_duration_epochs BIGINT DEFAULT NULL; diff --git a/harmony/harmonytask/harmonytask.go b/harmony/harmonytask/harmonytask.go index d78d0ee00..9fd3cfbb9 100644 --- a/harmony/harmonytask/harmonytask.go +++ b/harmony/harmonytask/harmonytask.go @@ -7,6 +7,9 @@ import ( "sync/atomic" "time" + "go.opencensus.io/stats" + "go.opencensus.io/tag" + "github.com/filecoin-project/curio/harmony/harmonydb" "github.com/filecoin-project/curio/harmony/resources" ) @@ -256,6 +259,8 @@ func (e *TaskEngine) GracefullyTerminate() { func (e *TaskEngine) poller() { nextWait := POLL_NEXT_DURATION for { + stats.Record(context.Background(), TaskMeasures.PollerIterations.M(1)) + select { case <-time.After(nextWait): // Find work periodically case <-e.ctx.Done(): ///////////////////// Graceful exit @@ -270,6 +275,22 @@ func (e *TaskEngine) poller() { if time.Since(e.lastFollowTime) > FOLLOW_FREQUENCY { e.followWorkInDB() } + + // update resource usage + availableResources := e.ResourcesAvailable() + totalResources := e.Resources() + + cpuUsage := 1 - float64(availableResources.Cpu)/float64(totalResources.Cpu) + stats.Record(context.Background(), TaskMeasures.CpuUsage.M(cpuUsage*100)) + + if totalResources.Gpu > 0 { + gpuUsage := 1 - availableResources.Gpu/totalResources.Gpu + stats.Record(context.Background(), TaskMeasures.GpuUsage.M(gpuUsage*100)) + } + + ramUsage := 1 - float64(availableResources.Ram)/float64(totalResources.Ram) + stats.Record(context.Background(), TaskMeasures.RamUsage.M(ramUsage*100)) + } } @@ -401,6 +422,13 @@ func (e *TaskEngine) Resources() resources.Resources { var Registry = map[string]TaskInterface{} func Reg(t TaskInterface) bool { - Registry[t.TypeDetails().Name] = t + name := t.TypeDetails().Name + Registry[name] = t + + // reset metrics + _ = stats.RecordWithTags(context.Background(), []tag.Mutator{ + tag.Upsert(taskNameTag, name), + }, TaskMeasures.ActiveTasks.M(0)) + return true } diff --git a/harmony/harmonytask/metrics.go b/harmony/harmonytask/metrics.go new file mode 100644 index 000000000..76a6b2e5a --- /dev/null +++ b/harmony/harmonytask/metrics.go @@ -0,0 +1,105 @@ +package harmonytask + +import ( + promclient "github.com/prometheus/client_golang/prometheus" + "go.opencensus.io/stats" + "go.opencensus.io/stats/view" + "go.opencensus.io/tag" +) + +var ( + taskNameTag, _ = tag.NewKey("task_name") + sourceTag, _ = tag.NewKey("source") + pre = "harmonytask_" + + // tasks can be short, but can extend to hours + durationBuckets = []float64{0.5, 1, 3, 6, 10, 20, 30, 60, 120, 300, 600, 1800, 3600, 7200, 18000, 36000} +) + +// TaskMeasures groups all harmonytask metrics. +var TaskMeasures = struct { + TasksStarted *stats.Int64Measure + TasksCompleted *stats.Int64Measure + TasksFailed *stats.Int64Measure + TaskDuration promclient.Histogram + ActiveTasks *stats.Int64Measure + CpuUsage *stats.Float64Measure + GpuUsage *stats.Float64Measure + RamUsage *stats.Float64Measure + PollerIterations *stats.Int64Measure + AddedTasks *stats.Int64Measure +}{ + TasksStarted: stats.Int64(pre+"tasks_started", "Total number of tasks started.", stats.UnitDimensionless), + TasksCompleted: stats.Int64(pre+"tasks_completed", "Total number of tasks completed successfully.", stats.UnitDimensionless), + TasksFailed: stats.Int64(pre+"tasks_failed", "Total number of tasks that failed.", stats.UnitDimensionless), + TaskDuration: promclient.NewHistogram(promclient.HistogramOpts{ + Name: pre + "task_duration_seconds", + Buckets: durationBuckets, + Help: "The histogram of task durations in seconds.", + }), + ActiveTasks: stats.Int64(pre+"active_tasks", "Current number of active tasks.", stats.UnitDimensionless), + CpuUsage: stats.Float64(pre+"cpu_usage", "Percentage of CPU in use.", stats.UnitDimensionless), + GpuUsage: stats.Float64(pre+"gpu_usage", "Percentage of GPU in use.", stats.UnitDimensionless), + RamUsage: stats.Float64(pre+"ram_usage", "Percentage of RAM in use.", stats.UnitDimensionless), + PollerIterations: stats.Int64(pre+"poller_iterations", "Total number of poller iterations.", stats.UnitDimensionless), + AddedTasks: stats.Int64(pre+"added_tasks", "Total number of tasks added.", stats.UnitDimensionless), +} + +// TaskViews groups all harmonytask-related default views. +func init() { + err := view.Register( + &view.View{ + Measure: TaskMeasures.TasksStarted, + Aggregation: view.Sum(), + TagKeys: []tag.Key{taskNameTag, sourceTag}, + }, + &view.View{ + Measure: TaskMeasures.TasksCompleted, + Aggregation: view.Sum(), + TagKeys: []tag.Key{taskNameTag}, + }, + &view.View{ + Measure: TaskMeasures.TasksFailed, + Aggregation: view.Sum(), + TagKeys: []tag.Key{taskNameTag}, + }, + &view.View{ + Measure: TaskMeasures.ActiveTasks, + Aggregation: view.LastValue(), + TagKeys: []tag.Key{taskNameTag}, + }, + &view.View{ + Measure: TaskMeasures.CpuUsage, + Aggregation: view.LastValue(), + TagKeys: []tag.Key{}, + }, + &view.View{ + Measure: TaskMeasures.GpuUsage, + Aggregation: view.LastValue(), + TagKeys: []tag.Key{}, + }, + &view.View{ + Measure: TaskMeasures.RamUsage, + Aggregation: view.LastValue(), + TagKeys: []tag.Key{}, + }, + &view.View{ + Measure: TaskMeasures.PollerIterations, + Aggregation: view.Sum(), + TagKeys: []tag.Key{}, + }, + &view.View{ + Measure: TaskMeasures.AddedTasks, + Aggregation: view.Sum(), + TagKeys: []tag.Key{taskNameTag}, + }, + ) + if err != nil { + panic(err) + } + + err = promclient.Register(TaskMeasures.TaskDuration) + if err != nil { + panic(err) + } +} diff --git a/harmony/harmonytask/task_type_handler.go b/harmony/harmonytask/task_type_handler.go index 642f29bb7..ed56c4d47 100644 --- a/harmony/harmonytask/task_type_handler.go +++ b/harmony/harmonytask/task_type_handler.go @@ -10,6 +10,8 @@ import ( "time" logging "github.com/ipfs/go-log/v2" + "go.opencensus.io/stats" + "go.opencensus.io/tag" "github.com/filecoin-project/curio/harmony/harmonydb" ) @@ -50,6 +52,13 @@ retryAddTask: log.Errorw("Could not add task. AddTasFunc failed", "error", err, "type", h.Name) return } + + err = stats.RecordWithTags(context.Background(), []tag.Mutator{ + tag.Upsert(taskNameTag, h.Name), + }, TaskMeasures.AddedTasks.M(1)) + if err != nil { + log.Errorw("Could not record added task", "error", err) + } } const ( @@ -154,7 +163,16 @@ canAcceptAgain: } } + _ = stats.RecordWithTags(context.Background(), []tag.Mutator{ + tag.Upsert(taskNameTag, h.Name), + tag.Upsert(sourceTag, from), + }, TaskMeasures.TasksStarted.M(1)) + h.Count.Add(1) + _ = stats.RecordWithTags(context.Background(), []tag.Mutator{ + tag.Upsert(taskNameTag, h.Name), + }, TaskMeasures.ActiveTasks.M(int64(h.Count.Load()))) + go func() { log.Infow("Beginning work on Task", "id", *tID, "from", from, "name", h.Name) @@ -204,6 +222,28 @@ canAcceptAgain: func (h *taskTypeHandler) recordCompletion(tID TaskID, workStart time.Time, done bool, doErr error) { workEnd := time.Now() retryWait := time.Millisecond * 100 + + { + // metrics + + _ = stats.RecordWithTags(context.Background(), []tag.Mutator{ + tag.Upsert(taskNameTag, h.Name), + }, TaskMeasures.ActiveTasks.M(int64(h.Count.Load()))) + + duration := workEnd.Sub(workStart).Seconds() + TaskMeasures.TaskDuration.Observe(duration) + + if done { + _ = stats.RecordWithTags(context.Background(), []tag.Mutator{ + tag.Upsert(taskNameTag, h.Name), + }, TaskMeasures.TasksCompleted.M(1)) + } else { + _ = stats.RecordWithTags(context.Background(), []tag.Mutator{ + tag.Upsert(taskNameTag, h.Name), + }, TaskMeasures.TasksFailed.M(1)) + } + } + retryRecordCompletion: cm, err := h.TaskEngine.db.BeginTransaction(h.TaskEngine.ctx, func(tx *harmonydb.Tx) (bool, error) { var postedTime time.Time @@ -277,6 +317,10 @@ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, tID, h.Name, postedTime.UTC(), workSta func (h *taskTypeHandler) AssertMachineHasCapacity() error { r := h.TaskEngine.ResourcesAvailable() + if h.Max > 0 && int(h.Count.Load()) >= h.Max { + return errors.New("Did not accept " + h.Name + " task: at max already") + } + if r.Cpu-h.Cost.Cpu < 0 { return errors.New("Did not accept " + h.Name + " task: out of cpu") } diff --git a/lib/hugepageutil/checkhuge.go b/lib/hugepageutil/checkhuge.go new file mode 100644 index 000000000..7ba2e503c --- /dev/null +++ b/lib/hugepageutil/checkhuge.go @@ -0,0 +1,51 @@ +package hugepageutil + +import ( + "bufio" + "os" + "strconv" + "strings" + + "golang.org/x/xerrors" +) + +func CheckHugePages(minPages int) error { + file, err := os.Open("/proc/meminfo") + if err != nil { + return xerrors.Errorf("error opening /proc/meminfo: %w", err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + hugepagesTotal := 0 + using1GHugepages := false + + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "HugePages_Total:") { + fields := strings.Fields(line) + if len(fields) >= 2 { + hugepagesTotal, err = strconv.Atoi(fields[1]) + if err != nil { + return xerrors.Errorf("error parsing HugePages_Total: %w", err) + } + } + } else if strings.Contains(line, "Hugepagesize:") && strings.Contains(line, "1048576 kB") { + using1GHugepages = true + } + } + + if err := scanner.Err(); err != nil { + return xerrors.Errorf("error reading /proc/meminfo: %w", err) + } + + if hugepagesTotal < minPages { + return xerrors.Errorf("insufficient hugepages: got %d, want at least %d", hugepagesTotal, minPages) + } + + if !using1GHugepages { + return xerrors.New("1G hugepages are not being used") + } + + return nil +} diff --git a/lib/metrics/exporter.go b/lib/metrics/exporter.go new file mode 100644 index 000000000..a098d71c8 --- /dev/null +++ b/lib/metrics/exporter.go @@ -0,0 +1,29 @@ +package metrics + +import ( + "net/http" + + "contrib.go.opencensus.io/exporter/prometheus" + promclient "github.com/prometheus/client_golang/prometheus" + log "github.com/sirupsen/logrus" +) + +func Exporter() http.Handler { + // Prometheus globals are exposed as interfaces, but the prometheus + // OpenCensus exporter expects a concrete *Registry. The concrete type of + // the globals are actually *Registry, so we downcast them, staying + // defensive in case things change under the hood. + registry, ok := promclient.DefaultRegisterer.(*promclient.Registry) + if !ok { + log.Warnf("failed to export default prometheus registry; some metrics will be unavailable; unexpected type: %T", promclient.DefaultRegisterer) + } + exporter, err := prometheus.NewExporter(prometheus.Options{ + Registry: registry, + Namespace: "curio", + }) + if err != nil { + log.Errorf("could not create the prometheus stats exporter: %v", err) + } + + return exporter +} diff --git a/lib/paths/db_index.go b/lib/paths/db_index.go index 6f56fe2db..50af18a6d 100644 --- a/lib/paths/db_index.go +++ b/lib/paths/db_index.go @@ -681,13 +681,18 @@ func (dbi *DBIndex) StorageInfo(ctx context.Context, id storiface.ID) (storiface func (dbi *DBIndex) StorageBestAlloc(ctx context.Context, allocate storiface.SectorFileType, ssize abi.SectorSize, pathType storiface.PathType, miner abi.ActorID) ([]storiface.StorageInfo, error) { var err error var spaceReq uint64 - switch pathType { - case storiface.PathSealing: - spaceReq, err = allocate.SealSpaceUse(ssize) - case storiface.PathStorage: - spaceReq, err = allocate.StoreSpaceUse(ssize) - default: - return nil, xerrors.Errorf("unexpected path type") + + if ctx.Value(SpaceUseKey) != nil { + spaceReq, err = (ctx.Value(SpaceUseKey).(SpaceUseFunc))(allocate, ssize) + } else { + switch pathType { + case storiface.PathSealing: + spaceReq, err = allocate.SealSpaceUse(ssize) + case storiface.PathStorage: + spaceReq, err = allocate.StoreSpaceUse(ssize) + default: + return nil, xerrors.Errorf("unexpected path type") + } } if err != nil { return nil, xerrors.Errorf("estimating required space: %w", err) diff --git a/lib/paths/fetch.go b/lib/paths/fetch.go index 55606f83a..d9a2a956a 100644 --- a/lib/paths/fetch.go +++ b/lib/paths/fetch.go @@ -14,6 +14,11 @@ import ( "github.com/filecoin-project/curio/lib/tarutil" ) +func init() { + tarutil.CacheFileConstraints["batch.json"] = 10_000 + tarutil.CacheFileConstraints["commit-phase1-output"] = 20_000_000 +} + func fetch(ctx context.Context, url, outname string, header http.Header) (rerr error) { log.Infof("Fetch %s -> %s", url, outname) diff --git a/lib/paths/index.go b/lib/paths/index.go index e09b887e0..2d2ec2654 100644 --- a/lib/paths/index.go +++ b/lib/paths/index.go @@ -29,6 +29,12 @@ var SkippedHeartbeatThresh = HeartbeatInterval * 5 //go:generate go run github.com/golang/mock/mockgen -destination=mocks/index.go -package=mocks . SectorIndex +type SpaceUseFunc func(ft storiface.SectorFileType, ssize abi.SectorSize) (uint64, error) + +type spaceUseCtxKey struct{} + +var SpaceUseKey = spaceUseCtxKey{} + type SectorIndex interface { // part of storage-miner api StorageAttach(context.Context, storiface.StorageInfo, fsutil.FsStat) error StorageDetach(ctx context.Context, id storiface.ID, url string) error diff --git a/lib/paths/local.go b/lib/paths/local.go index 76c98fd30..38e45a6a7 100644 --- a/lib/paths/local.go +++ b/lib/paths/local.go @@ -1,6 +1,7 @@ package paths import ( + "bytes" "context" "encoding/json" "math/bits" @@ -15,9 +16,13 @@ import ( "golang.org/x/xerrors" ffi "github.com/filecoin-project/filecoin-ffi" + commcid "github.com/filecoin-project/go-fil-commcid" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/proof" + cuproof "github.com/filecoin-project/curio/lib/proof" + "github.com/filecoin-project/curio/lib/supraffi" + "github.com/filecoin-project/lotus/lib/result" "github.com/filecoin-project/lotus/storage/sealer/fsutil" "github.com/filecoin-project/lotus/storage/sealer/storiface" @@ -36,9 +41,20 @@ type LocalStorage interface { const MetaFile = "sectorstore.json" const SnapVproofFile = "snap-vproof.json" +const BatchMetaFile = "batch.json" // supraseal const MinFreeStoragePercentage = float64(0) +const CommitPhase1OutputFileSupra = "commit-phase1-output" + +type BatchMeta struct { + SupraSeal bool + BlockOffset uint64 + NumInPipeline int + + BatchSectors int +} + type Local struct { localStorage LocalStorage index SectorIndex @@ -680,6 +696,8 @@ func (st *Local) AcquireSector(ctx context.Context, sid storiface.SectorRef, exi } if best == "" { + log.Warnw("allocate failed", "id", sid, "type", fileType, "pathType", pathType, "op", op, "sis", sis) + return storiface.SectorPaths{}, storiface.SectorPaths{}, storiface.Err(storiface.ErrTempAllocateSpace, xerrors.Errorf("couldn't find a suitable path for a sector")) } @@ -973,6 +991,15 @@ func (st *Local) GeneratePoRepVanillaProof(ctx context.Context, sr storiface.Sec return nil, xerrors.Errorf("getting sector size: %w", err) } + { + // check if the sector is part of a supraseal batch with data in raw block storage + // does BatchMetaFile exist in cache? + batchMetaPath := filepath.Join(src.Cache, BatchMetaFile) + if _, err := os.Stat(batchMetaPath); err == nil { + return st.supraPoRepVanillaProof(src, sr, sealed, unsealed, ticket, seed) + } + } + secPiece := []abi.PieceInfo{{ Size: abi.PaddedPieceSize(ssize), PieceCID: unsealed, @@ -999,4 +1026,82 @@ func (st *Local) ReadSnapVanillaProof(ctx context.Context, sr storiface.SectorRe return out, nil } +func (st *Local) supraPoRepVanillaProof(src storiface.SectorPaths, sr storiface.SectorRef, sealed, unsealed cid.Cid, ticket abi.SealRandomness, seed abi.InteractiveSealRandomness) ([]byte, error) { + batchMetaPath := filepath.Join(src.Cache, BatchMetaFile) + bmdata, err := os.ReadFile(batchMetaPath) + if err != nil { + return nil, xerrors.Errorf("read batch meta file: %w", err) + } + + var bm BatchMeta + if err := json.Unmarshal(bmdata, &bm); err != nil { + return nil, xerrors.Errorf("unmarshal batch meta file: %w", err) + } + + commd, err := commcid.CIDToDataCommitmentV1(unsealed) + if err != nil { + return nil, xerrors.Errorf("unsealed cid to data commitment: %w", err) + } + + replicaID, err := sr.ProofType.ReplicaId(sr.ID.Miner, sr.ID.Number, ticket, commd) + if err != nil { + return nil, xerrors.Errorf("replica id: %w", err) + } + + ssize, err := sr.ProofType.SectorSize() + if err != nil { + return nil, xerrors.Errorf("sector size: %w", err) + } + + // C1 writes the output to a file, so we need to read it back. + // Outputs to cachePath/commit-phase1-output + // NOTE: that file is raw, and rust C1 returns a json repr, so we need to translate first + + // first see if commit-phase1-output is there + commitPhase1OutputPath := filepath.Join(src.Cache, CommitPhase1OutputFileSupra) + if _, err := os.Stat(commitPhase1OutputPath); err != nil { + if !os.IsNotExist(err) { + return nil, xerrors.Errorf("stat commit phase1 output: %w", err) + } + + parentsPath, err := ParentsForProof(sr.ProofType) + if err != nil { + return nil, xerrors.Errorf("parents for proof: %w", err) + } + + // not found, compute it + res := supraffi.C1(bm.BlockOffset, bm.BatchSectors, bm.NumInPipeline, replicaID[:], seed, ticket, src.Cache, parentsPath, src.Sealed, uint64(ssize)) + if res != 0 { + return nil, xerrors.Errorf("c1 failed: %d", res) + } + + // check again + if _, err := os.Stat(commitPhase1OutputPath); err != nil { + return nil, xerrors.Errorf("stat commit phase1 output after compute: %w", err) + } + } + + // read the output + rawOut, err := os.ReadFile(commitPhase1OutputPath) + if err != nil { + return nil, xerrors.Errorf("read commit phase1 output: %w", err) + } + + // decode + dec, err := cuproof.DecodeCommit1OutRaw(bytes.NewReader(rawOut)) + if err != nil { + return nil, xerrors.Errorf("decode commit phase1 output: %w", err) + } + + log.Infow("supraPoRepVanillaProof", "sref", sr, "replicaID", replicaID, "seed", seed, "ticket", ticket, "decrepl", dec.ReplicaID, "decr", dec.CommR, "decd", dec.CommD) + + // out is json, so we need to marshal it back + out, err := json.Marshal(dec) + if err != nil { + return nil, xerrors.Errorf("marshal commit phase1 output: %w", err) + } + + return out, nil +} + var _ Store = &Local{} diff --git a/lib/paths/proof_parents.go b/lib/paths/proof_parents.go new file mode 100644 index 000000000..9c530650b --- /dev/null +++ b/lib/paths/proof_parents.go @@ -0,0 +1,32 @@ +package paths + +import ( + "errors" + "os" + "path/filepath" + + "github.com/filecoin-project/go-state-types/abi" +) + +var parentCacheDir string + +func init() { + if os.Getenv("FIL_PROOFS_PARENT_CACHE") != "" { + parentCacheDir = os.Getenv("FIL_PROOFS_PARENT_CACHE") + } else { + parentCacheDir = "/var/tmp/filecoin-parents" + } +} + +func ParentsForProof(spt abi.RegisteredSealProof) (string, error) { + switch spt { + case abi.RegisteredSealProof_StackedDrg2KiBV1_1: + return filepath.Join(parentCacheDir, "v28-sdr-parent-494d91dc80f2df5272c4b9e129bc7ade9405225993af9fe34e6542a39a47554b.cache"), nil + case abi.RegisteredSealProof_StackedDrg512MiBV1_1: + return filepath.Join(parentCacheDir, "v28-sdr-parent-7ba215a1d2345774ab90b8cb1158d296e409d6068819d7b8c7baf0b25d63dc34.cache"), nil + case abi.RegisteredSealProof_StackedDrg32GiBV1_1: + return filepath.Join(parentCacheDir, "v28-sdr-parent-21981246c370f9d76c7a77ab273d94bde0ceb4e938292334960bce05585dc117.cache"), nil + default: + return "", errors.New("unsupported proof type") + } +} diff --git a/lib/proof/porep_vproof_bin_decode.go b/lib/proof/porep_vproof_bin_decode.go new file mode 100644 index 000000000..985d6a87e --- /dev/null +++ b/lib/proof/porep_vproof_bin_decode.go @@ -0,0 +1,411 @@ +package proof + +import ( + "encoding/binary" + "fmt" + "io" +) + +// This file contains a bincode decoder for Commit1OutRaw. +// This is the format output by the C++ supraseal C1 implementation. +// bincode - https://github.com/bincode-org/bincode + +func ReadLE[T any](r io.Reader) (T, error) { + var out T + err := binary.Read(r, binary.LittleEndian, &out) + return out, err +} + +func DecodeCommit1OutRaw(r io.Reader) (Commit1OutRaw, error) { + var out Commit1OutRaw + var err error + + out.RegisteredProof = "StackedDrg32GiBV1_1" + + // VanillaProofs + out.VanillaProofs = make(map[StringRegisteredProofType][][]VanillaStackedProof) + var vpOuterLength uint64 + if err = binary.Read(r, binary.LittleEndian, &vpOuterLength); err != nil { + return out, fmt.Errorf("failed to read VanillaProofs outer length: %w", err) + } + + for i := uint64(0); i < vpOuterLength; i++ { + var vpInnerLength uint64 + if err = binary.Read(r, binary.LittleEndian, &vpInnerLength); err != nil { + return out, fmt.Errorf("failed to read VanillaProofs inner length: %w", err) + } + + proofs := make([]VanillaStackedProof, vpInnerLength) + for j := uint64(0); j < vpInnerLength; j++ { + proofs[j], err = DecodeVanillaStackedProof(r) + if err != nil { + return out, fmt.Errorf("failed to decode VanillaStackedProof: %w", err) + } + } + + key := StringRegisteredProofType("StackedDrg32GiBV1") + out.VanillaProofs[key] = append(out.VanillaProofs[key], proofs) + } + + // CommR + if out.CommR, err = DecodeCommitment(r); err != nil { + return out, fmt.Errorf("failed to decode CommR: %w", err) + } + + // CommD + if out.CommD, err = DecodeCommitment(r); err != nil { + return out, fmt.Errorf("failed to decode CommD: %w", err) + } + + // ReplicaID + if out.ReplicaID, err = DecodeCommitment(r); err != nil { + return out, fmt.Errorf("failed to decode ReplicaID: %w", err) + } + + // Seed + if out.Seed, err = DecodeTicket(r); err != nil { + return out, fmt.Errorf("failed to decode Seed: %w", err) + } + + // Ticket + if out.Ticket, err = DecodeTicket(r); err != nil { + return out, fmt.Errorf("failed to decode Ticket: %w", err) + } + + // Read last byte, require EOF + if _, err := r.Read(make([]byte, 1)); err != io.EOF { + return out, fmt.Errorf("expected EOF") + } + + return out, nil +} + +func DecodeVanillaStackedProof(r io.Reader) (VanillaStackedProof, error) { + var out VanillaStackedProof + var err error + + if out.CommDProofs, err = DecodeMerkleProof[Sha256Domain](r); err != nil { + return out, fmt.Errorf("failed to decode CommDProofs: %w", err) + } + if out.CommRLastProof, err = DecodeMerkleProof[PoseidonDomain](r); err != nil { + return out, fmt.Errorf("failed to decode CommRLastProof: %w", err) + } + if out.ReplicaColumnProofs, err = DecodeReplicaColumnProof[PoseidonDomain](r); err != nil { + return out, fmt.Errorf("failed to decode ReplicaColumnProofs: %w", err) + } + + var numLabelingProofs uint64 + if err = binary.Read(r, binary.LittleEndian, &numLabelingProofs); err != nil { + return out, fmt.Errorf("failed to read number of LabelingProofs: %w", err) + } + out.LabelingProofs = make([]LabelingProof[PoseidonDomain], numLabelingProofs) + for i := uint64(0); i < numLabelingProofs; i++ { + out.LabelingProofs[i], err = DecodeLabelingProof[PoseidonDomain](r) + if err != nil { + return out, fmt.Errorf("failed to decode LabelingProof: %w", err) + } + } + + out.EncodingProof, err = DecodeEncodingProof[PoseidonDomain](r) + if err != nil { + return out, fmt.Errorf("failed to decode EncodingProof: %w", err) + } + + return out, nil +} + +func DecodeMerkleProof[H HasherDomain](r io.Reader) (MerkleProof[H], error) { + var out MerkleProof[H] + var err error + + out.Data, err = DecodeProofData[H](r) + if err != nil { + return out, fmt.Errorf("failed to decode ProofData: %w", err) + } + + return out, nil +} + +func DecodeProofData[H HasherDomain](r io.Reader) (ProofData[H], error) { + var out ProofData[H] + var err error + + proofType, err := ReadLE[uint32](r) + if err != nil { + return out, fmt.Errorf("failed to read proof type: %w", err) + } + + switch proofType { + case 0: + out.Single = new(SingleProof[H]) + *out.Single, err = DecodeSingleProof[H](r) + case 1: + out.Sub = new(SubProof[H]) + *out.Sub, err = DecodeSubProof[H](r) + case 2: + out.Top = new(TopProof[H]) + *out.Top, err = DecodeTopProof[H](r) + default: + return out, fmt.Errorf("unknown proof type: %d", proofType) + } + + if err != nil { + return out, fmt.Errorf("failed to decode proof: %w", err) + } + + return out, nil +} + +func DecodeSingleProof[H HasherDomain](r io.Reader) (SingleProof[H], error) { + var out SingleProof[H] + var err error + + if out.Root, err = DecodeHasherDomain[H](r); err != nil { + return out, fmt.Errorf("failed to decode Root: %w", err) + } + if out.Leaf, err = DecodeHasherDomain[H](r); err != nil { + return out, fmt.Errorf("failed to decode Leaf: %w", err) + } + if out.Path, err = DecodeInclusionPath[H](r); err != nil { + return out, fmt.Errorf("failed to decode Path: %w", err) + } + + return out, nil +} + +func DecodeSubProof[H HasherDomain](r io.Reader) (SubProof[H], error) { + var out SubProof[H] + var err error + + if out.BaseProof, err = DecodeInclusionPath[H](r); err != nil { + return out, fmt.Errorf("failed to decode BaseProof: %w", err) + } + if out.SubProof, err = DecodeInclusionPath[H](r); err != nil { + return out, fmt.Errorf("failed to decode SubProof: %w", err) + } + if out.Root, err = DecodeHasherDomain[H](r); err != nil { + return out, fmt.Errorf("failed to decode Root: %w", err) + } + if out.Leaf, err = DecodeHasherDomain[H](r); err != nil { + return out, fmt.Errorf("failed to decode Leaf: %w", err) + } + + return out, nil +} + +func DecodeTopProof[H HasherDomain](r io.Reader) (TopProof[H], error) { + var out TopProof[H] + var err error + + if out.BaseProof, err = DecodeInclusionPath[H](r); err != nil { + return out, fmt.Errorf("failed to decode BaseProof: %w", err) + } + if out.SubProof, err = DecodeInclusionPath[H](r); err != nil { + return out, fmt.Errorf("failed to decode SubProof: %w", err) + } + if out.TopProof, err = DecodeInclusionPath[H](r); err != nil { + return out, fmt.Errorf("failed to decode TopProof: %w", err) + } + if out.Root, err = DecodeHasherDomain[H](r); err != nil { + return out, fmt.Errorf("failed to decode Root: %w", err) + } + if out.Leaf, err = DecodeHasherDomain[H](r); err != nil { + return out, fmt.Errorf("failed to decode Leaf: %w", err) + } + + return out, nil +} + +func DecodeInclusionPath[H HasherDomain](r io.Reader) (InclusionPath[H], error) { + var out InclusionPath[H] + var err error + + numElements, err := ReadLE[uint64](r) + if err != nil { + return out, fmt.Errorf("failed to read number of elements: %w", err) + } + + out.Path = make([]PathElement[H], numElements) + for i := uint64(0); i < numElements; i++ { + out.Path[i], err = DecodePathElement[H](r) + if err != nil { + return out, fmt.Errorf("failed to decode PathElement: %w", err) + } + } + + return out, nil +} + +func DecodePathElement[H HasherDomain](r io.Reader) (PathElement[H], error) { + var out PathElement[H] + var err error + + numHashes, err := ReadLE[uint64](r) + if err != nil { + return out, fmt.Errorf("failed to read number of hashes: %w", err) + } + + out.Hashes = make([]H, numHashes) + for i := uint64(0); i < numHashes; i++ { + out.Hashes[i], err = DecodeHasherDomain[H](r) + if err != nil { + return out, fmt.Errorf("failed to decode HasherDomain: %w", err) + } + } + + out.Index, err = ReadLE[uint64](r) + if err != nil { + return out, fmt.Errorf("failed to read index: %w", err) + } + + return out, nil +} + +func DecodeReplicaColumnProof[H HasherDomain](r io.Reader) (ReplicaColumnProof[H], error) { + var out ReplicaColumnProof[H] + var err error + + if out.C_X, err = DecodeColumnProof[H](r); err != nil { + return out, fmt.Errorf("failed to decode C_X: %w", err) + } + + numDrgParents, err := ReadLE[uint64](r) + if err != nil { + return out, fmt.Errorf("failed to read number of DRG parents: %w", err) + } + out.DrgParents = make([]ColumnProof[H], numDrgParents) + for i := uint64(0); i < numDrgParents; i++ { + out.DrgParents[i], err = DecodeColumnProof[H](r) + if err != nil { + return out, fmt.Errorf("failed to decode DRG parent: %w", err) + } + } + + numExpParents, err := ReadLE[uint64](r) + if err != nil { + return out, fmt.Errorf("failed to read number of EXP parents: %w", err) + } + out.ExpParents = make([]ColumnProof[H], numExpParents) + for i := uint64(0); i < numExpParents; i++ { + out.ExpParents[i], err = DecodeColumnProof[H](r) + if err != nil { + return out, fmt.Errorf("failed to decode EXP parent: %w", err) + } + } + + return out, nil +} + +func DecodeColumnProof[H HasherDomain](r io.Reader) (ColumnProof[H], error) { + var out ColumnProof[H] + var err error + + if out.Column, err = DecodeColumn[H](r); err != nil { + return out, fmt.Errorf("failed to decode Column: %w", err) + } + if out.InclusionProof, err = DecodeMerkleProof[H](r); err != nil { + return out, fmt.Errorf("failed to decode InclusionProof: %w", err) + } + + return out, nil +} + +func DecodeColumn[H HasherDomain](r io.Reader) (Column[H], error) { + var out Column[H] + var err error + + if err = binary.Read(r, binary.LittleEndian, &out.Index); err != nil { + return out, fmt.Errorf("failed to decode Index: %w", err) + } + + var rowsLength uint64 + if err = binary.Read(r, binary.LittleEndian, &rowsLength); err != nil { + return out, fmt.Errorf("failed to read Rows length: %w", err) + } + out.Rows = make([]H, rowsLength) + for i := uint64(0); i < rowsLength; i++ { + if out.Rows[i], err = DecodeHasherDomain[H](r); err != nil { + return out, fmt.Errorf("failed to decode Row: %w", err) + } + } + + // Note: We're ignoring the _h field as it's not used in Go + + return out, nil +} + +func DecodeLabelingProof[H HasherDomain](r io.Reader) (LabelingProof[H], error) { + var out LabelingProof[H] + var err error + + var parentsLength uint64 + if err = binary.Read(r, binary.LittleEndian, &parentsLength); err != nil { + return out, fmt.Errorf("failed to read Parents length: %w", err) + } + out.Parents = make([]H, parentsLength) + for i := uint64(0); i < parentsLength; i++ { + if out.Parents[i], err = DecodeHasherDomain[H](r); err != nil { + return out, fmt.Errorf("failed to decode Parent: %w", err) + } + } + + if err = binary.Read(r, binary.LittleEndian, &out.LayerIndex); err != nil { + return out, fmt.Errorf("failed to decode LayerIndex: %w", err) + } + + if err = binary.Read(r, binary.LittleEndian, &out.Node); err != nil { + return out, fmt.Errorf("failed to decode Node: %w", err) + } + + return out, nil +} + +func DecodeEncodingProof[H HasherDomain](r io.Reader) (EncodingProof[H], error) { + var out EncodingProof[H] + var err error + + var parentsLength uint64 + if err = binary.Read(r, binary.LittleEndian, &parentsLength); err != nil { + return out, fmt.Errorf("failed to read Parents length: %w", err) + } + out.Parents = make([]H, parentsLength) + for i := uint64(0); i < parentsLength; i++ { + if out.Parents[i], err = DecodeHasherDomain[H](r); err != nil { + return out, fmt.Errorf("failed to decode Parent: %w", err) + } + } + + if err = binary.Read(r, binary.LittleEndian, &out.LayerIndex); err != nil { + return out, fmt.Errorf("failed to decode LayerIndex: %w", err) + } + + if err = binary.Read(r, binary.LittleEndian, &out.Node); err != nil { + return out, fmt.Errorf("failed to decode Node: %w", err) + } + + return out, nil +} + +func DecodeHasherDomain[H HasherDomain](r io.Reader) (H, error) { + var out H + if err := binary.Read(r, binary.LittleEndian, &out); err != nil { + return out, fmt.Errorf("failed to decode HasherDomain: %w", err) + } + return out, nil +} + +func DecodeTicket(r io.Reader) (Ticket, error) { + var out Ticket + if _, err := io.ReadFull(r, out[:]); err != nil { + return out, fmt.Errorf("failed to decode Ticket: %w", err) + } + return out, nil +} + +func DecodeCommitment(r io.Reader) (Commitment, error) { + var out Commitment + if _, err := io.ReadFull(r, out[:]); err != nil { + return out, fmt.Errorf("failed to decode Commitment: %w", err) + } + return out, nil +} diff --git a/lib/proof/porep_vproof_bin_test.go b/lib/proof/porep_vproof_bin_test.go new file mode 100644 index 000000000..d5662d2aa --- /dev/null +++ b/lib/proof/porep_vproof_bin_test.go @@ -0,0 +1,157 @@ +package proof + +import ( + "bytes" + "compress/gzip" + "encoding/json" + "io" + "os" + "testing" + + "github.com/filecoin-project/filecoin-ffi/cgo" +) + +func TestDecode(t *testing.T) { + if os.Getenv("EXPENSIVE_TESTS") == "" { + t.Skip() + } + + //binFile := "../../extern/supra_seal/demos/c2-test/resources/test/commit-phase1-output" + binFile := "../../commit-phase1-output.gz" + + gzData, err := os.ReadFile(binFile) + if err != nil { + t.Fatal(err) + } + + gzReader, err := gzip.NewReader(bytes.NewReader(gzData)) + if err != nil { + t.Fatal(err) + } + + rawData, err := io.ReadAll(gzReader) + if err != nil { + t.Fatal(err) + } + + if err := gzReader.Close(); err != nil { + t.Fatal(err) + } + + dec, err := DecodeCommit1OutRaw(bytes.NewReader(rawData)) + if err != nil { + t.Fatal(err) + } + + p1o, err := json.Marshal(dec) + if err != nil { + t.Fatal(err) + } + + var proverID = [32]byte{9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9} + pba := cgo.AsByteArray32(proverID[:]) + + _, err = cgo.SealCommitPhase2(cgo.AsSliceRefUint8(p1o), uint64(0), &pba) + if err != nil { + t.Fatal(err) + } +} + +/* +// bin/main.rs +use storage_proofs_core::settings::SETTINGS; +use anyhow::{Context, Result}; +use std::fs::{read, write}; +use std::path::PathBuf; +use bincode::deserialize; +use serde_json; +use filecoin_proofs_v1::{SealCommitPhase1Output, SectorShape32GiB}; +use filecoin_proofs_api::RegisteredSealProof::StackedDrg32GiBV1_1; +use filecoin_proofs_api::RegisteredSealProof::StackedDrg32GiBV1; + +use filecoin_proofs_api::seal::{SealCommitPhase1Output as SealCommitPhase1OutputOut, VanillaSealProof}; + +fn main() -> Result<()> { + println!("{:#?}", *SETTINGS); + + let commit_phase1_output_path = PathBuf::from("/tmp/c1o"); + + let commit_phase1_output_bytes = read(&commit_phase1_output_path) + .with_context(|| { + format!( + "couldn't read file commit_phase1_output_path={:?}", + commit_phase1_output_path + ) + })?; + println!( + "commit_phase1_output_bytes len {}", + commit_phase1_output_bytes.len() + ); + + let res: SealCommitPhase1Output = + deserialize(&commit_phase1_output_bytes)?; + + let SealCommitPhase1Output:: { + vanilla_proofs, + comm_r, + comm_d, + replica_id, + seed, + ticket, + } = res; + + let registered_proof= StackedDrg32GiBV1_1; + + let res2: SealCommitPhase1OutputOut = SealCommitPhase1OutputOut{ + registered_proof, + vanilla_proofs: VanillaSealProof::from_raw::(StackedDrg32GiBV1, &vanilla_proofs)?, + comm_r, + comm_d, + replica_id: replica_id.into(), + seed, + ticket, + }; + + let result = serde_json::to_vec(&res2)?; + + let output_path = PathBuf::from("/tmp/c1o.json"); + write(&output_path, result) + .with_context(|| format!("couldn't write to file output_path={:?}", output_path))?; + + Ok(()) +} + +func TestDecodeSNRustDec(t *testing.T) { + //binFile := "../../extern/supra_seal/demos/c2-test/resources/test/commit-phase1-output" + binFile := "../../commit-phase1-output.json" + + rawData, err := os.ReadFile(binFile) + if err != nil { + t.Fatal(err) + } + + var dec Commit1OutRaw + err = json.Unmarshal(rawData, &dec) + if err != nil { + t.Fatal(err) + } + + commr := dec.CommR + commd := dec.CommD + + sl, _ := commcid.ReplicaCommitmentV1ToCID(commr[:]) + us, _ := commcid.DataCommitmentV1ToCID(commd[:]) + + fmt.Println("sealed:", sl) + fmt.Println("unsealed:", us) + fmt.Printf("replicaid: %x\n", dec.ReplicaID) + + var proverID = [32]byte{9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9} + pba := cgo.AsByteArray32(proverID[:]) + + _, err = cgo.SealCommitPhase2(cgo.AsSliceRefUint8(rawData), uint64(0), &pba) + if err != nil { + t.Fatal(err) + } +} +*/ diff --git a/lib/proof/porep_vproof_challenges.go b/lib/proof/porep_vproof_challenges.go new file mode 100644 index 000000000..1124cf461 --- /dev/null +++ b/lib/proof/porep_vproof_challenges.go @@ -0,0 +1,68 @@ +package proof + +import ( + "encoding/binary" + "math/big" + + "github.com/minio/sha256-simd" +) + +// TODO: This file is a placeholder with links to the original implementation in Rust. Eventually we want to +// have our own implementation for generating PoRep vanilla proofs in Go. + +// https://github.com/filecoin-project/rust-fil-proofs/blob/8f5bd86be36a55e33b9b293ba22ea13ca1f28163/storage-proofs-porep/src/stacked/vanilla/challenges.rs#L21 + +func DeriveInteractiveChallenges( + challengesPerPartition uint64, + sectorNnodes SectorNodes, + ReplicaID Commitment, + Seed Ticket, + k uint8, +) []uint64 { + var jbuf [4]byte + + out := make([]uint64, challengesPerPartition) + + for i := uint64(0); i < challengesPerPartition; i++ { + // let j: u32 = ((self.challenges_per_partition * k as usize) + i) as u32; + j := uint32((challengesPerPartition * uint64(k)) + i) + + /* + let hash = Sha256::new() + .chain_update(replica_id.into_bytes()) + .chain_update(seed) + .chain_update(j.to_le_bytes()) + .finalize(); + let bigint = BigUint::from_bytes_le(hash.as_ref()); + bigint_to_challenge(bigint, sector_nodes) + */ + hasher := sha256.New() + hasher.Write(ReplicaID[:]) + hasher.Write(Seed[:]) + + binary.LittleEndian.PutUint32(jbuf[:], j) + hasher.Write(jbuf[:]) + + hash := hasher.Sum(nil) + bigint := new(big.Int).SetBytes(hash) // SetBytes is big-endian + + out[i] = bigintToChallenge(bigint, sectorNnodes) + } + + return out +} + +/* + fn bigint_to_challenge(bigint: BigUint, sector_nodes: usize) -> usize { + debug_assert!(sector_nodes < 1 << 32); + // Ensure that we don't challenge the first node. + let non_zero_node = (bigint % (sector_nodes - 1)) + 1usize; + non_zero_node.to_u32_digits()[0] as usize + } +*/ +func bigintToChallenge(bigint *big.Int, sectorNodes SectorNodes) uint64 { + // Ensure that we don't challenge the first node. + nonZeroNode := new(big.Int).Mod(bigint, big.NewInt(int64(sectorNodes-1))) + nonZeroNode.Add(nonZeroNode, big.NewInt(1)) + return nonZeroNode.Uint64() +} diff --git a/lib/proof/porep_vproof_test.go b/lib/proof/porep_vproof_test.go new file mode 100644 index 000000000..4407f77f9 --- /dev/null +++ b/lib/proof/porep_vproof_test.go @@ -0,0 +1,171 @@ +package proof + +import ( + "bytes" + "crypto/rand" + "encoding/json" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + + ffi "github.com/filecoin-project/filecoin-ffi" + commcid "github.com/filecoin-project/go-fil-commcid" + "github.com/filecoin-project/go-state-types/abi" +) + +func TestRoundtripPorepVproof(t *testing.T) { + // seal a sector (random data) + testDir := t.TempDir() + + paddedSize := abi.PaddedPieceSize(2048) + unpaddedSize := paddedSize.Unpadded() + + // generate a random sector + sectorData := make([]byte, unpaddedSize) + _, err := rand.Read(sectorData) + require.NoError(t, err) + + unsFile := filepath.Join(testDir, "unsealed") + cacheFile := filepath.Join(testDir, "cache") + sealedFile := filepath.Join(testDir, "sealed") + + { + err = os.MkdirAll(cacheFile, 0755) + require.NoError(t, err) + f, err := os.Create(sealedFile) + require.NoError(t, err) + err = f.Close() + require.NoError(t, err) + } + + commd, err := BuildTreeD(bytes.NewReader(sectorData), true, unsFile, 2048) + require.NoError(t, err) + fmt.Println("D:", commd) + + err = os.Truncate(unsFile, int64(paddedSize)) // prefix contains exactly unsealed data + require.NoError(t, err) + + spt := abi.RegisteredSealProof_StackedDrg2KiBV1_1 + num := abi.SectorNumber(234) + miner := abi.ActorID(123) + + var ticket [32]byte + _, err = rand.Read(ticket[:]) + require.NoError(t, err) + ticket[31] &= 0x3f // fr32 + + pieces := []abi.PieceInfo{{ + Size: paddedSize, + PieceCID: commd, + }} + + p1o, err := ffi.SealPreCommitPhase1(spt, cacheFile, unsFile, sealedFile, num, miner, ticket[:], pieces) + require.NoError(t, err) + + sealed, unsealed, err := ffi.SealPreCommitPhase2(p1o, cacheFile, sealedFile) + require.NoError(t, err) + _ = sealed + _ = unsealed + + // generate a proof + var seed [32]byte + _, err = rand.Read(seed[:]) + require.NoError(t, err) + seed[31] &= 0x3f // fr32 + + c1out, err := ffi.SealCommitPhase1(spt, sealed, unsealed, cacheFile, sealedFile, num, miner, ticket[:], seed[:], pieces) + require.NoError(t, err) + + // deserialize the proof with Go types + var realVProof Commit1OutRaw + err = json.Unmarshal(c1out, &realVProof) + require.NoError(t, err) + + t.Run("json-roundtrip-check", func(t *testing.T) { + // serialize the proof to JSON + proof1out, err := json.Marshal(realVProof) + require.NoError(t, err) + + // check that the JSON is as expected + var rustObj, goObj map[string]interface{} + err = json.Unmarshal(c1out, &rustObj) + require.NoError(t, err) + err = json.Unmarshal(proof1out, &goObj) + require.NoError(t, err) + + diff := cmp.Diff(rustObj, goObj) + if !cmp.Equal(rustObj, goObj) { + t.Errorf("proof mismatch: %s", diff) + } + + require.True(t, cmp.Equal(rustObj, goObj)) + }) + + t.Run("check-toplevel", func(t *testing.T) { + /* + type Commit1OutRaw struct { + CommD Commitment `json:"comm_d"` // CHECK + CommR Commitment `json:"comm_r"` // CHECK + RegisteredProof StringRegisteredProofType `json:"registered_proof"` // CHECK + ReplicaID Commitment `json:"replica_id"` // CHECK + Seed Ticket `json:"seed"` // CHECK + Ticket Ticket `json:"ticket"` // CHECK + VanillaProofs map[StringRegisteredProofType][][]VanillaStackedProof `json:"vanilla_proofs"` + } + */ + + rawCommD, err := commcid.CIDToDataCommitmentV1(unsealed) + require.NoError(t, err) + rawCommR, err := commcid.CIDToReplicaCommitmentV1(sealed) + require.NoError(t, err) + + require.Equal(t, realVProof.CommD, Commitment(rawCommD)) + require.Equal(t, realVProof.CommR, Commitment(rawCommR)) + + require.Equal(t, realVProof.RegisteredProof, StringRegisteredProofType("StackedDrg2KiBV1_1")) + + replicaID, err := spt.ReplicaId(miner, num, ticket[:], realVProof.CommD[:]) + require.NoError(t, err) + require.Equal(t, realVProof.ReplicaID, Commitment(replicaID)) + + require.Equal(t, realVProof.Seed, Ticket(seed)) + require.Equal(t, realVProof.Ticket, Ticket(ticket)) + }) + + /*t.Run("check-d-proofs", func(t *testing.T) { + require.Len(t, realVProof.VanillaProofs, 1) + + expected := extractDProofs(realVProof.VanillaProofs["StackedDrg2KiBV1"]) // fun fact: this doesn't have _1 in the name... + + // todo compute + extract + var actual [][]MerkleProof[Sha256Domain] + + requireNoDiff(t, expected, actual) + })*/ +} + +/*func extractDProofs(vp [][]VanillaStackedProof) [][]MerkleProof[Sha256Domain] { + var out [][]MerkleProof[Sha256Domain] + for _, v := range vp { + var proofs []MerkleProof[Sha256Domain] + for _, p := range v { + proofs = append(proofs, p.CommDProofs) + } + out = append(out, proofs) + } + return out +} + +func requireNoDiff(t *testing.T, rustObj, goObj interface{}) { + diff := cmp.Diff(rustObj, goObj) + if !cmp.Equal(rustObj, goObj) { + t.Errorf("proof mismatch: %s", diff) + } + + require.True(t, cmp.Equal(rustObj, goObj)) +} +*/ diff --git a/lib/proof/porep_vproof_types.go b/lib/proof/porep_vproof_types.go new file mode 100644 index 000000000..24a829af9 --- /dev/null +++ b/lib/proof/porep_vproof_types.go @@ -0,0 +1,427 @@ +package proof + +// This file contains PoRep vanilla proof type definitions from +// - https://github.com/filecoin-project/rust-fil-proofs/tree/master/storage-proofs-core/src/merkle +// - https://github.com/filecoin-project/rust-fil-proofs/tree/master/storage-proofs-porep/src/stacked/vanilla +// - https://github.com/filecoin-project/rust-filecoin-proofs-api/tree/master/src +// The json representation of those matches the representation expected by rust-fil-proofs. + +// core + +type Commitment [32]byte +type Ticket [32]byte + +type StringRegisteredProofType string // e.g. "StackedDrg2KiBV1" + +type HasherDomain = any + +type Sha256Domain [32]byte + +type PoseidonDomain [32]byte // Fr + +/* +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct MerkleProof< + H: Hasher, + BaseArity: PoseidonArity, + SubTreeArity: PoseidonArity = U0, + TopTreeArity: PoseidonArity = U0, +> { + #[serde(bound( + serialize = "H::Domain: Serialize", + deserialize = "H::Domain: Deserialize<'de>" + ))] + data: ProofData, +} +*/ + +type MerkleProof[H HasherDomain] struct { + Data ProofData[H] `json:"data"` +} + +/* +#[derive(Debug, Clone, Serialize, Deserialize)] +enum ProofData< + H: Hasher, + BaseArity: PoseidonArity, + SubTreeArity: PoseidonArity, + TopTreeArity: PoseidonArity, +> { + #[serde(bound( + serialize = "H::Domain: Serialize", + deserialize = "H::Domain: Deserialize<'de>" + ))] + Single(SingleProof), + #[serde(bound( + serialize = "H::Domain: Serialize", + deserialize = "H::Domain: Deserialize<'de>" + ))] + Sub(SubProof), + #[serde(bound( + serialize = "H::Domain: Serialize", + deserialize = "H::Domain: Deserialize<'de>" + ))] + Top(TopProof), +} +*/ + +type ProofData[H HasherDomain] struct { + Single *SingleProof[H] `json:"Single,omitempty"` + Sub *SubProof[H] `json:"Sub,omitempty"` + Top *TopProof[H] `json:"Top,omitempty"` +} + +/* +struct SingleProof { + /// Root of the merkle tree. + #[serde(bound( + serialize = "H::Domain: Serialize", + deserialize = "H::Domain: Deserialize<'de>" + ))] + root: H::Domain, + /// The original leaf data for this prof. + #[serde(bound( + serialize = "H::Domain: Serialize", + deserialize = "H::Domain: Deserialize<'de>" + ))] + leaf: H::Domain, + /// The path from leaf to root. + #[serde(bound( + serialize = "H::Domain: Serialize", + deserialize = "H::Domain: Deserialize<'de>" + ))] + path: InclusionPath, +} +*/ + +type SingleProof[H HasherDomain] struct { + Root H `json:"root"` + Leaf H `json:"leaf"` + Path InclusionPath[H] `json:"path"` +} + +/* +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +struct SubProof { + #[serde(bound( + serialize = "H::Domain: Serialize", + deserialize = "H::Domain: Deserialize<'de>" + ))] + base_proof: InclusionPath, + #[serde(bound( + serialize = "H::Domain: Serialize", + deserialize = "H::Domain: Deserialize<'de>" + ))] + sub_proof: InclusionPath, + /// Root of the merkle tree. + #[serde(bound( + serialize = "H::Domain: Serialize", + deserialize = "H::Domain: Deserialize<'de>" + ))] + root: H::Domain, + /// The original leaf data for this prof. + #[serde(bound( + serialize = "H::Domain: Serialize", + deserialize = "H::Domain: Deserialize<'de>" + ))] + leaf: H::Domain, +} + +*/ + +type SubProof[H HasherDomain] struct { + BaseProof InclusionPath[H] `json:"base_proof"` + SubProof InclusionPath[H] `json:"sub_proof"` + Root H `json:"root"` + Leaf H `json:"leaf"` +} + +/* +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +struct TopProof< + H: Hasher, + BaseArity: PoseidonArity, + SubTreeArity: PoseidonArity, + TopTreeArity: PoseidonArity, +> { + #[serde(bound( + serialize = "H::Domain: Serialize", + deserialize = "H::Domain: Deserialize<'de>" + ))] + base_proof: InclusionPath, + #[serde(bound( + serialize = "H::Domain: Serialize", + deserialize = "H::Domain: Deserialize<'de>" + ))] + sub_proof: InclusionPath, + #[serde(bound( + serialize = "H::Domain: Serialize", + deserialize = "H::Domain: Deserialize<'de>" + ))] + top_proof: InclusionPath, + /// Root of the merkle tree. + #[serde(bound( + serialize = "H::Domain: Serialize", + deserialize = "H::Domain: Deserialize<'de>" + ))] + root: H::Domain, + /// The original leaf data for this prof. + #[serde(bound( + serialize = "H::Domain: Serialize", + deserialize = "H::Domain: Deserialize<'de>" + ))] + leaf: H::Domain, +} +*/ + +type TopProof[H HasherDomain] struct { + BaseProof InclusionPath[H] `json:"base_proof"` + SubProof InclusionPath[H] `json:"sub_proof"` + TopProof InclusionPath[H] `json:"top_proof"` + + Root H `json:"root"` + Leaf H `json:"leaf"` +} + +/* +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct InclusionPath { + #[serde(bound( + serialize = "H::Domain: Serialize", + deserialize = "H::Domain: Deserialize<'de>" + ))] + path: Vec>, +} +*/ + +type InclusionPath[H HasherDomain] struct { + Path []PathElement[H] `json:"path"` +} + +/* +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct PathElement { + #[serde(bound( + serialize = "H::Domain: Serialize", + deserialize = "H::Domain: Deserialize<'de>" + ))] + hashes: Vec, + index: usize, + #[serde(skip)] + _arity: PhantomData, +} +*/ + +type PathElement[H HasherDomain] struct { + Hashes []H `json:"hashes"` + Index uint64 `json:"index"` +} + +// porep + +type Label struct { + ID string `json:"id"` + Path string `json:"path"` + RowsToDiscard int `json:"rows_to_discard"` + Size int `json:"size"` +} + +type Labels struct { + H any `json:"_h"` // todo ? + Labels []Label `json:"labels"` +} + +type PreCommit1OutRaw struct { + LotusSealRand []byte `json:"_lotus_SealRandomness"` + + CommD Commitment `json:"comm_d"` + Config Label `json:"config"` + Labels map[StringRegisteredProofType]Labels `json:"labels"` + RegisteredProof StringRegisteredProofType `json:"registered_proof"` +} + +/* Commit1OutRaw maps to: +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SealCommitPhase1Output { + pub registered_proof: RegisteredSealProof, + pub vanilla_proofs: VanillaSealProof, + pub comm_r: Commitment, + pub comm_d: Commitment, + pub replica_id: ::Domain, + pub seed: Ticket, + pub ticket: Ticket, +} +*/ + +type Commit1OutRaw struct { + CommD Commitment `json:"comm_d"` + CommR Commitment `json:"comm_r"` + RegisteredProof StringRegisteredProofType `json:"registered_proof"` + ReplicaID Commitment `json:"replica_id"` + Seed Ticket `json:"seed"` + Ticket Ticket `json:"ticket"` + + // ProofType -> [partitions] -> [challenge_index?] -> Proof + VanillaProofs map[StringRegisteredProofType][][]VanillaStackedProof `json:"vanilla_proofs"` +} + +/* +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum VanillaSealProof { + StackedDrg2KiBV1(Vec>>), + StackedDrg8MiBV1(Vec>>), + StackedDrg512MiBV1(Vec>>), + StackedDrg32GiBV1(Vec>>), + StackedDrg64GiBV1(Vec>>), +} + +//VanillaSealProof as RawVanillaSealProof +pub type VanillaSealProof = stacked::Proof; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Proof { + #[serde(bound( + serialize = "MerkleProof: Serialize", + deserialize = "MerkleProof: Deserialize<'de>" + ))] + pub comm_d_proofs: MerkleProof, + #[serde(bound( + serialize = "MerkleProof: Serialize", + deserialize = "MerkleProof: Deserialize<'de>" + ))] + pub comm_r_last_proof: + MerkleProof, + #[serde(bound( + serialize = "ReplicaColumnProof,>: Serialize", + deserialize = "ReplicaColumnProof>: Deserialize<'de>" + ))] + pub replica_column_proofs: ReplicaColumnProof< + MerkleProof, + >, + #[serde(bound( + serialize = "LabelingProof: Serialize", + deserialize = "LabelingProof: Deserialize<'de>" + ))] + /// Indexed by layer in 1..layers. + pub labeling_proofs: Vec>, + #[serde(bound( + serialize = "EncodingProof: Serialize", + deserialize = "EncodingProof: Deserialize<'de>" + ))] + pub encoding_proof: EncodingProof, +} + +*/ + +type VanillaStackedProof struct { + CommDProofs MerkleProof[Sha256Domain] `json:"comm_d_proofs"` + CommRLastProof MerkleProof[PoseidonDomain] `json:"comm_r_last_proof"` + + ReplicaColumnProofs ReplicaColumnProof[PoseidonDomain] `json:"replica_column_proofs"` + LabelingProofs []LabelingProof[PoseidonDomain] `json:"labeling_proofs"` + EncodingProof EncodingProof[PoseidonDomain] `json:"encoding_proof"` +} + +/* +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReplicaColumnProof { + #[serde(bound( + serialize = "ColumnProof: Serialize", + deserialize = "ColumnProof: Deserialize<'de>" + ))] + pub c_x: ColumnProof, + #[serde(bound( + serialize = "ColumnProof: Serialize", + deserialize = "ColumnProof: Deserialize<'de>" + ))] + pub drg_parents: Vec>, + #[serde(bound( + serialize = "ColumnProof: Serialize", + deserialize = "ColumnProof: Deserialize<'de>" + ))] + pub exp_parents: Vec>, +} +*/ + +type ReplicaColumnProof[H HasherDomain] struct { + C_X ColumnProof[H] `json:"c_x"` + DrgParents []ColumnProof[H] `json:"drg_parents"` + ExpParents []ColumnProof[H] `json:"exp_parents"` +} + +/* +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ColumnProof { + #[serde(bound( + serialize = "Column: Serialize", + deserialize = "Column: Deserialize<'de>" + ))] + pub(crate) column: Column, + #[serde(bound( + serialize = "Proof: Serialize", + deserialize = "Proof: DeserializeOwned" + ))] + pub(crate) inclusion_proof: Proof, +} +*/ + +type ColumnProof[H HasherDomain] struct { + Column Column[H] `json:"column"` + InclusionProof MerkleProof[H] `json:"inclusion_proof"` +} + +/* +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct Column { + pub(crate) index: u32, + pub(crate) rows: Vec, + _h: PhantomData, +} +*/ + +type Column[H HasherDomain] struct { + Index uint32 `json:"index"` + Rows []H `json:"rows"` + H any `json:"_h"` +} + +/* +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LabelingProof { + pub(crate) parents: Vec, + pub(crate) layer_index: u32, + pub(crate) node: u64, + #[serde(skip)] + _h: PhantomData, +} +*/ + +type LabelingProof[H HasherDomain] struct { + Parents []H `json:"parents"` + LayerIndex uint32 `json:"layer_index"` + Node uint64 `json:"node"` + //H any `json:"_h"` +} + +/* +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EncodingProof { + pub(crate) parents: Vec, + pub(crate) layer_index: u32, + pub(crate) node: u64, + #[serde(skip)] + _h: PhantomData, +} +*/ + +type EncodingProof[H HasherDomain] struct { + Parents []H `json:"parents"` + LayerIndex uint32 `json:"layer_index"` + Node uint64 `json:"node"` + //H any `json:"_h"` +} + +const NODE_SIZE = 32 + +// SectorNodes is sector size as node count +type SectorNodes uint64 diff --git a/lib/proof/porep_vproof_vanilla.go b/lib/proof/porep_vproof_vanilla.go new file mode 100644 index 000000000..2bcea8cc9 --- /dev/null +++ b/lib/proof/porep_vproof_vanilla.go @@ -0,0 +1,14 @@ +package proof + +// TODO: This file is a placeholder with links to the original implementation in Rust. Eventually we want to +// have our own implementation for generating PoRep vanilla proofs in Go. + +// https://github.com/filecoin-project/rust-fil-proofs/blob/8f5bd86be36a55e33b9b293ba22ea13ca1f28163/storage-proofs-porep/src/stacked/vanilla/proof_scheme.rs#L60 +func ProveAllPartitions() { + +} + +// https://github.com/filecoin-project/rust-fil-proofs/blob/8f5bd86be36a55e33b9b293ba22ea13ca1f28163/storage-proofs-porep/src/stacked/vanilla/proof.rs#L96 +func ProveLayers() { + +} diff --git a/lib/proof/sn-comp-sector-seal/main.go b/lib/proof/sn-comp-sector-seal/main.go new file mode 100644 index 000000000..9f4a0b57d --- /dev/null +++ b/lib/proof/sn-comp-sector-seal/main.go @@ -0,0 +1,217 @@ +package main + +import ( + "encoding/binary" + "fmt" + "math/bits" + "os" + "path/filepath" + + "github.com/minio/sha256-simd" + "golang.org/x/xerrors" + + ffi "github.com/filecoin-project/filecoin-ffi" + "github.com/filecoin-project/filecoin-ffi/cgo" + "github.com/filecoin-project/go-commp-utils/zerocomm" + commcid "github.com/filecoin-project/go-fil-commcid" + "github.com/filecoin-project/go-state-types/abi" +) + +func main() { + if len(os.Args) != 2 || os.Args[1] == "-h" || os.Args[1] == "--help" { + fmt.Println("This tool creates a 32GiB sector compatible with SupraSeal demo sector output") + fmt.Println("Useful only for development purposes.") + fmt.Printf("Usage: %s \n", os.Args[0]) + return + } + + outPath := os.Args[1] + + /*const ssize = 32 << 30 + const profTyp = cgo.RegisteredSealProofStackedDrg32GiBV11*/ + + const ssize = 32 << 30 + const profTyp = cgo.RegisteredSealProofStackedDrg32GiBV11 + + cacheDirPath := filepath.Join(outPath, "cache") + stagedSectorPath := filepath.Join(outPath, "unseal") + sealedSectorPath := filepath.Join(outPath, "seal") + + _ = os.MkdirAll(cacheDirPath, 0755) + + { + file, err := os.Create(stagedSectorPath) + if err != nil { + fmt.Printf("Failed to create file: %v\n", err) + return + } + + size := int64(ssize) + zero := make([]byte, max(1024*1024, ssize)) + var written int64 + for written < size { + n, err := file.Write(zero) + if err != nil { + fmt.Printf("Failed to write to file: %v\n", err) + return + } + written += int64(n) + } + + if err := file.Close(); err != nil { + panic(err) + } + } + + { + file, err := os.Create(sealedSectorPath) + if err != nil { + fmt.Printf("Failed to create file: %v\n", err) + return + } + + size := int64(ssize) + zero := make([]byte, max(1024*1024, ssize)) + var written int64 + for written < size { + n, err := file.Write(zero) + if err != nil { + fmt.Printf("Failed to write to file: %v\n", err) + return + } + written += int64(n) + } + + if err := file.Close(); err != nil { + panic(err) + } + } + + sectorNum := uint64(0) + var proverID = [32]byte{9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9} + var ticket = [32]byte{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1} + + /*var proverID [32]byte + proverID[2] = 4 + var ticket [32]byte + rand.Read(ticket[:]) + ticket[31] &= 0x3c*/ + + cprover := cgo.AsByteArray32(proverID[:]) + cticket := cgo.AsByteArray32(ticket[:]) + + level := bits.TrailingZeros64(uint64(ssize)) - zerocomm.Skip - 5 + + ppi := []cgo.PublicPieceInfo{ + cgo.NewPublicPieceInfo(uint64(abi.PaddedPieceSize(ssize).Unpadded()), cgo.AsByteArray32(zerocomm.PieceComms[level][:])), + } + + replid, _ := ReplicaId(abi.SectorNumber(sectorNum), ticket[:], zerocomm.PieceComms[level][:]) + fmt.Printf("replid: %x\n", replid) + for i := range replid { + fmt.Printf("%d, ", replid[i]) + } + fmt.Println() + + p1o, err := cgo.SealPreCommitPhase1(profTyp, + cgo.AsSliceRefUint8([]byte(cacheDirPath)), + cgo.AsSliceRefUint8([]byte(stagedSectorPath)), + cgo.AsSliceRefUint8([]byte(sealedSectorPath)), sectorNum, &cprover, &cticket, cgo.AsSliceRefPublicPieceInfo(ppi)) + if err != nil { + panic(err) + } + + err = os.WriteFile(filepath.Join(cacheDirPath, "pc1out.json"), p1o, 0666) + if err != nil { + panic(err) + } + + scid, ucid, err := ffi.SealPreCommitPhase2(p1o, cacheDirPath, sealedSectorPath) + if err != nil { + panic(err) + } + + fmt.Println("sealed:", scid) + fmt.Println("unsealed:", ucid) + + commr, err := commcid.CIDToReplicaCommitmentV1(scid) + if err != nil { + panic(err) + } + ccommr := cgo.AsByteArray32(commr) + + commd, err := commcid.CIDToDataCommitmentV1(ucid) + if err != nil { + panic(err) + } + ccommd := cgo.AsByteArray32(commd) + + c1o, err := cgo.SealCommitPhase1(profTyp, &ccommr, &ccommd, + cgo.AsSliceRefUint8([]byte(cacheDirPath)), + cgo.AsSliceRefUint8([]byte(sealedSectorPath)), + sectorNum, + &cprover, + &cticket, + &cticket, + cgo.AsSliceRefPublicPieceInfo(ppi)) + if err != nil { + panic(err) + } + + err = os.WriteFile(filepath.Join(cacheDirPath, "commit1-out.json"), c1o, 0666) + if err != nil { + panic(err) + } + + proof, err := cgo.SealCommitPhase2(cgo.AsSliceRefUint8(c1o), sectorNum, &cprover) + if err != nil { + panic(err) + } + + err = os.WriteFile(filepath.Join(cacheDirPath, "proof.json"), proof, 0666) + if err != nil { + panic(err) + } + + fmt.Println("done!") +} + +func ReplicaId(sector abi.SectorNumber, ticket []byte, commd []byte) ([32]byte, error) { + // https://github.com/filecoin-project/rust-fil-proofs/blob/5b46d4ac88e19003416bb110e2b2871523cc2892/storage-proofs-porep/src/stacked/vanilla/params.rs#L758-L775 + + pi := [32]byte{9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9} + porepID, err := abi.RegisteredSealProof_StackedDrg32GiBV1_1.PoRepID() + if err != nil { + return [32]byte{}, err + } + + if len(ticket) != 32 { + return [32]byte{}, xerrors.Errorf("invalid ticket length %d", len(ticket)) + } + if len(commd) != 32 { + return [32]byte{}, xerrors.Errorf("invalid commd length %d", len(commd)) + } + + var sectorID [8]byte + binary.BigEndian.PutUint64(sectorID[:], uint64(sector)) + + s := sha256.New() + + // sha256 writes never error + _, _ = s.Write(pi[:]) + _, _ = s.Write(sectorID[:]) + _, _ = s.Write(ticket) + _, _ = s.Write(commd) + _, _ = s.Write(porepID[:]) + + return bytesIntoFr32Safe(s.Sum(nil)), nil +} + +func bytesIntoFr32Safe(in []byte) [32]byte { + var out [32]byte + copy(out[:], in) + + out[31] &= 0b0011_1111 + + return out +} diff --git a/lib/slotmgr/metrics.go b/lib/slotmgr/metrics.go new file mode 100644 index 000000000..b5baaf1a0 --- /dev/null +++ b/lib/slotmgr/metrics.go @@ -0,0 +1,48 @@ +package slotmgr + +import ( + "go.opencensus.io/stats" + "go.opencensus.io/stats/view" +) + +var ( + pre = "slotmgr_" +) + +// SlotMgrMeasures groups all slotmgr metrics. +var SlotMgrMeasures = struct { + SlotsAvailable *stats.Int64Measure + SlotsAcquired *stats.Int64Measure + SlotsReleased *stats.Int64Measure + SlotErrors *stats.Int64Measure +}{ + SlotsAvailable: stats.Int64(pre+"slots_available", "Number of available slots.", stats.UnitDimensionless), + SlotsAcquired: stats.Int64(pre+"slots_acquired", "Total number of slots acquired.", stats.UnitDimensionless), + SlotsReleased: stats.Int64(pre+"slots_released", "Total number of slots released.", stats.UnitDimensionless), + SlotErrors: stats.Int64(pre+"slot_errors", "Total number of slot errors (e.g., failed to put).", stats.UnitDimensionless), +} + +// init registers the views for slotmgr metrics. +func init() { + err := view.Register( + &view.View{ + Measure: SlotMgrMeasures.SlotsAvailable, + Aggregation: view.LastValue(), + }, + &view.View{ + Measure: SlotMgrMeasures.SlotsAcquired, + Aggregation: view.Sum(), + }, + &view.View{ + Measure: SlotMgrMeasures.SlotsReleased, + Aggregation: view.Sum(), + }, + &view.View{ + Measure: SlotMgrMeasures.SlotErrors, + Aggregation: view.Sum(), + }, + ) + if err != nil { + panic(err) + } +} diff --git a/lib/slotmgr/slotmgr.go b/lib/slotmgr/slotmgr.go new file mode 100644 index 000000000..f8b9445ed --- /dev/null +++ b/lib/slotmgr/slotmgr.go @@ -0,0 +1,43 @@ +package slotmgr + +import ( + "context" + + "go.opencensus.io/stats" + "golang.org/x/xerrors" +) + +type SlotMgr struct { + Slots chan uint64 +} + +const maxPipelines = 2 + +func NewSlotMgr() *SlotMgr { + slots := make(chan uint64, maxPipelines) + stats.Record(context.Background(), SlotMgrMeasures.SlotsAvailable.M(int64(maxPipelines))) + return &SlotMgr{slots} +} + +func (s *SlotMgr) Get() uint64 { + slot := <-s.Slots + stats.Record(context.Background(), SlotMgrMeasures.SlotsAcquired.M(1)) + stats.Record(context.Background(), SlotMgrMeasures.SlotsAvailable.M(int64(s.Available()))) + return slot +} + +func (s *SlotMgr) Put(slot uint64) error { + select { + case s.Slots <- slot: + stats.Record(context.Background(), SlotMgrMeasures.SlotsReleased.M(1)) + stats.Record(context.Background(), SlotMgrMeasures.SlotsAvailable.M(int64(s.Available()))) + return nil + default: + stats.Record(context.Background(), SlotMgrMeasures.SlotErrors.M(1)) + return xerrors.Errorf("slot list full, max %d", cap(s.Slots)) + } +} + +func (s *SlotMgr) Available() int { + return len(s.Slots) +} diff --git a/lib/supraffi/no_supraseal.go b/lib/supraffi/no_supraseal.go new file mode 100644 index 000000000..5e2cfd293 --- /dev/null +++ b/lib/supraffi/no_supraseal.go @@ -0,0 +1,118 @@ +//go:build !supraseal + +package supraffi + +import ( + "bytes" + "encoding/binary" +) + +// SupraSealInit initializes the supra seal with a sector size and optional config file. +func SupraSealInit(sectorSize uint64, configFile string) { + panic("SupraSealInit: supraseal build tag not enabled") +} + +// Pc1 performs the pc1 operation. +func Pc1(blockOffset uint64, replicaIDs [][32]byte, parentsFilename string, sectorSize uint64) int { + panic("Pc1: supraseal build tag not enabled") +} + +type Path struct { + Replica string + Cache string +} + +// GenerateMultiString generates a //multi// string from an array of Path structs +func GenerateMultiString(paths []Path) (string, error) { + var buffer bytes.Buffer + buffer.WriteString("//multi//") + + for _, path := range paths { + replicaPath := []byte(path.Replica) + cachePath := []byte(path.Cache) + + // Write the length and path for the replica + if err := binary.Write(&buffer, binary.LittleEndian, uint32(len(replicaPath))); err != nil { + return "", err + } + buffer.Write(replicaPath) + + // Write the length and path for the cache + if err := binary.Write(&buffer, binary.LittleEndian, uint32(len(cachePath))); err != nil { + return "", err + } + buffer.Write(cachePath) + } + + return buffer.String(), nil +} + +// Pc2 performs the pc2 operation. +func Pc2(blockOffset uint64, numSectors int, outputDir string, sectorSize uint64) int { + panic("Pc2: supraseal build tag not enabled") +} + +// Pc2Cleanup deletes files associated with pc2. +func Pc2Cleanup(numSectors int, outputDir string, sectorSize uint64) int { + panic("Pc2Cleanup: supraseal build tag not enabled") +} + +// C1 performs the c1 operation. +func C1(blockOffset uint64, numSectors, sectorSlot int, replicaID, seed, ticket []byte, cachePath, parentsFilename, replicaPath string, sectorSize uint64) int { + panic("C1: supraseal build tag not enabled") +} + +// GetMaxBlockOffset returns the highest available block offset. +func GetMaxBlockOffset(sectorSize uint64) uint64 { + panic("GetMaxBlockOffset: supraseal build tag not enabled") +} + +// GetSlotSize returns the size in blocks required for the given number of sectors. +func GetSlotSize(numSectors int, sectorSize uint64) uint64 { + panic("GetSlotSize: supraseal build tag not enabled") +} + +// GetCommCFromTree returns comm_c after calculating from tree file(s). +func GetCommCFromTree(commC []byte, cachePath string, sectorSize uint64) bool { + panic("GetCommCFromTree: supraseal build tag not enabled") +} + +// GetCommC returns comm_c from p_aux file. +func GetCommC(commC []byte, cachePath string) bool { + panic("GetCommC: supraseal build tag not enabled") +} + +// SetCommC sets comm_c in the p_aux file. +func SetCommC(commC []byte, cachePath string) bool { + panic("SetCommC: supraseal build tag not enabled") +} + +// GetCommRLastFromTree returns comm_r_last after calculating from tree file(s). +func GetCommRLastFromTree(commRLast []byte, cachePath string, sectorSize uint64) bool { + panic("GetCommRLastFromTree: supraseal build tag not enabled") +} + +// GetCommRLast returns comm_r_last from p_aux file. +func GetCommRLast(commRLast []byte, cachePath string) bool { + panic("GetCommRLast: supraseal build tag not enabled") +} + +// SetCommRLast sets comm_r_last in the p_aux file. +func SetCommRLast(commRLast []byte, cachePath string) bool { + panic("SetCommRLast: supraseal build tag not enabled") +} + +// GetCommR returns comm_r after calculating from p_aux file. +func GetCommR(commR []byte, cachePath string) bool { + panic("GetCommR: supraseal build tag not enabled") +} + +// GetCommD returns comm_d from tree_d file. +func GetCommD(commD []byte, cachePath string) bool { + panic("GetCommD: supraseal build tag not enabled") +} + +// GetCCCommD returns comm_d for a cc sector. +func GetCCCommD(commD []byte, sectorSize int) bool { + panic("GetCCCommD: supraseal build tag not enabled") +} diff --git a/lib/supraffi/seal.go b/lib/supraffi/seal.go new file mode 100644 index 000000000..2af840d22 --- /dev/null +++ b/lib/supraffi/seal.go @@ -0,0 +1,304 @@ +//go:build supraseal + +package supraffi + +/* + #cgo CFLAGS: -I${SRCDIR}/../../extern/supra_seal/sealing + #cgo LDFLAGS: -fno-omit-frame-pointer -Wl,-z,noexecstack -Wl,-z,relro,-z,now -fuse-ld=bfd -L${SRCDIR}/../../extern/supra_seal/obj -L${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/build/lib -L${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/isa-l/.libs -lsupraseal -Wl,--whole-archive -Wl,--no-as-needed -lspdk_bdev_malloc -lspdk_bdev_null -lspdk_bdev_nvme -lspdk_bdev_passthru -lspdk_bdev_lvol -lspdk_bdev_raid -lspdk_bdev_error -lspdk_bdev_gpt -lspdk_bdev_split -lspdk_bdev_delay -lspdk_bdev_zone_block -lspdk_blobfs_bdev -lspdk_blobfs -lspdk_blob_bdev -lspdk_lvol -lspdk_blob -lspdk_nvme -lspdk_bdev_ftl -lspdk_ftl -lspdk_bdev_aio -lspdk_bdev_virtio -lspdk_virtio -lspdk_vfio_user -lspdk_accel_ioat -lspdk_ioat -lspdk_scheduler_dynamic -lspdk_env_dpdk -lspdk_scheduler_dpdk_governor -lspdk_scheduler_gscheduler -lspdk_sock_posix -lspdk_event -lspdk_event_bdev -lspdk_bdev -lspdk_notify -lspdk_dma -lspdk_event_accel -lspdk_accel -lspdk_event_vmd -lspdk_vmd -lspdk_event_sock -lspdk_init -lspdk_thread -lspdk_trace -lspdk_sock -lspdk_rpc -lspdk_jsonrpc -lspdk_json -lspdk_util -lspdk_log -Wl,--no-whole-archive ${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/build/lib/libspdk_env_dpdk.a -Wl,--whole-archive ${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_bus_pci.a ${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_cryptodev.a ${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_dmadev.a ${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_eal.a ${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_ethdev.a ${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_hash.a ${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_kvargs.a ${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_mbuf.a ${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_mempool.a ${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_mempool_ring.a ${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_net.a ${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_pci.a ${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_power.a ${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_rcu.a ${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_ring.a ${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_telemetry.a ${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_vhost.a -Wl,--no-whole-archive -lnuma -lisal -pthread -ldl -lrt -luuid -lssl -lcrypto -lm -laio -lcudart_static -L${SRCDIR}/../../extern/supra_seal/deps/blst -lblst -lconfig++ -lgmp -lstdc++ + #include + #include + #include "supra_seal.h" + #include +*/ +import "C" +import ( + "bytes" + "encoding/binary" + "unsafe" +) + +/* +root = {SRCDIR}/../../extern/supra_seal/ + ++ c++ -Ideps/spdk-v22.09/include -Ideps/spdk-v22.09/isa-l/.. -Ideps/spdk-v22.09/dpdk/build/include +-g -O2 -march=native -fPIC -fno-omit-frame-pointer -fno-strict-aliasing -fstack-protector -fno-common +-D_GNU_SOURCE -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2 +-DSPDK_GIT_COMMIT=4be6d3043 +-pthread -Wall -Wextra -Wno-unused-variable -Wno-unused-parameter -Wno-missing-field-initializers -Wformat -Wformat-security +-Ideps/spdk-v22.09/include -Ideps/spdk-v22.09/isa-l/.. -Ideps/spdk-v22.09/dpdk/build/include +-Iposeidon -Ideps/sppark -Ideps/sppark/util -Ideps/blst/src -c sealing/supra_seal.cpp -o obj/supra_seal.o -Wno-subobject-linkage + +--- +NOTE: The below lines match the top of the file, just in a moderately more readable form. + +-#cgo LDFLAGS: +-fno-omit-frame-pointer +-Wl,-z,relro,-z,now +-Wl,-z,noexecstack +-fuse-ld=bfd +-L${SRCDIR}/../../extern/supra_seal/obj +-L${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/build/lib +-L${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/isa-l/.libs +-lsupraseal +-Wl,--whole-archive +-Wl,--no-as-needed +-lspdk_bdev_malloc +-lspdk_bdev_null +-lspdk_bdev_nvme +-lspdk_bdev_passthru +-lspdk_bdev_lvol +-lspdk_bdev_raid +-lspdk_bdev_error +-lspdk_bdev_gpt +-lspdk_bdev_split +-lspdk_bdev_delay +-lspdk_bdev_zone_block +-lspdk_blobfs_bdev +-lspdk_blobfs +-lspdk_blob_bdev +-lspdk_lvol +-lspdk_blob +-lspdk_nvme +-lspdk_bdev_ftl +-lspdk_ftl +-lspdk_bdev_aio +-lspdk_bdev_virtio +-lspdk_virtio +-lspdk_vfio_user +-lspdk_accel_ioat +-lspdk_ioat +-lspdk_scheduler_dynamic +-lspdk_env_dpdk +-lspdk_scheduler_dpdk_governor +-lspdk_scheduler_gscheduler +-lspdk_sock_posix +-lspdk_event +-lspdk_event_bdev +-lspdk_bdev +-lspdk_notify +-lspdk_dma +-lspdk_event_accel +-lspdk_accel +-lspdk_event_vmd +-lspdk_vmd +-lspdk_event_sock +-lspdk_init +-lspdk_thread +-lspdk_trace +-lspdk_sock +-lspdk_rpc +-lspdk_jsonrpc +-lspdk_json +-lspdk_util +-lspdk_log +-Wl,--no-whole-archive +${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/build/lib/libspdk_env_dpdk.a +-Wl,--whole-archive +${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_bus_pci.a +${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_cryptodev.a +${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_dmadev.a +${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_eal.a +${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_ethdev.a +${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_hash.a +${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_kvargs.a +${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_mbuf.a +${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_mempool.a +${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_mempool_ring.a +${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_net.a +${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_pci.a +${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_power.a +${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_rcu.a +${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_ring.a +${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_telemetry.a +${SRCDIR}/../../extern/supra_seal/deps/spdk-v22.09/dpdk/build/lib/librte_vhost.a +-Wl,--no-whole-archive +-lnuma +-lisal +-pthread +-ldl +-lrt +-luuid +-lssl +-lcrypto +-lm +-laio +-lcudart_static +-L${SRCDIR}/../../extern/supra_seal/deps/blst -lblst +-lconfig++ +-lgmp +-lstdc++ + +*/ + +// SupraSealInit initializes the supra seal with a sector size and optional config file. +func SupraSealInit(sectorSize uint64, configFile string) { + cConfigFile := C.CString(configFile) + defer C.free(unsafe.Pointer(cConfigFile)) + C.supra_seal_init(C.size_t(sectorSize), cConfigFile) +} + +// Pc1 performs the pc1 operation. +func Pc1(blockOffset uint64, replicaIDs [][32]byte, parentsFilename string, sectorSize uint64) int { + flatReplicaIDs := make([]byte, len(replicaIDs)*32) + for i, id := range replicaIDs { + copy(flatReplicaIDs[i*32:], id[:]) + } + numSectors := len(replicaIDs) + + cReplicaIDs := (*C.uint8_t)(unsafe.Pointer(&flatReplicaIDs[0])) + cParentsFilename := C.CString(parentsFilename) + defer C.free(unsafe.Pointer(cParentsFilename)) + return int(C.pc1(C.uint64_t(blockOffset), C.size_t(numSectors), cReplicaIDs, cParentsFilename, C.size_t(sectorSize))) +} + +type Path struct { + Replica string + Cache string +} + +// GenerateMultiString generates a //multi// string from an array of Path structs +func GenerateMultiString(paths []Path) (string, error) { + var buffer bytes.Buffer + buffer.WriteString("//multi//") + + for _, path := range paths { + replicaPath := []byte(path.Replica) + cachePath := []byte(path.Cache) + + // Write the length and path for the replica + if err := binary.Write(&buffer, binary.LittleEndian, uint32(len(replicaPath))); err != nil { + return "", err + } + buffer.Write(replicaPath) + + // Write the length and path for the cache + if err := binary.Write(&buffer, binary.LittleEndian, uint32(len(cachePath))); err != nil { + return "", err + } + buffer.Write(cachePath) + } + + return buffer.String(), nil +} + +// Pc2 performs the pc2 operation. +func Pc2(blockOffset uint64, numSectors int, outputDir string, sectorSize uint64) int { + /* + int pc2(size_t block_offset, size_t num_sectors, const char* output_dir, + const char** data_filenames, size_t sector_size); + */ + cOutputDir := C.CString(outputDir) + defer C.free(unsafe.Pointer(cOutputDir)) + + // data filenames is for unsealed data to be encoded + // https://github.com/supranational/supra_seal/blob/a64e4060fbffea68adc0ac4512062e5a03e76048/pc2/cuda/pc2.cu#L329 + // not sure if that works correctly, but that's where we could encode data in the future + // for now pass a null as the pointer to the array of filenames + + var cDataFilenames **C.char + cDataFilenames = nil + + return int(C.pc2(C.size_t(blockOffset), C.size_t(numSectors), cOutputDir, cDataFilenames, C.size_t(sectorSize))) +} + +// Pc2Cleanup deletes files associated with pc2. +func Pc2Cleanup(numSectors int, outputDir string, sectorSize uint64) int { + cOutputDir := C.CString(outputDir) + defer C.free(unsafe.Pointer(cOutputDir)) + return int(C.pc2_cleanup(C.size_t(numSectors), cOutputDir, C.size_t(sectorSize))) +} + +// C1 performs the c1 operation. +// Outputs to cachePath/commit-phase1-output +func C1(blockOffset uint64, numSectors, sectorSlot int, replicaID, seed, ticket []byte, cachePath, parentsFilename, replicaPath string, sectorSize uint64) int { + cReplicaID := (*C.uint8_t)(unsafe.Pointer(&replicaID[0])) + cSeed := (*C.uint8_t)(unsafe.Pointer(&seed[0])) + cTicket := (*C.uint8_t)(unsafe.Pointer(&ticket[0])) + cCachePath := C.CString(cachePath) + cParentsFilename := C.CString(parentsFilename) + cReplicaPath := C.CString(replicaPath) + defer C.free(unsafe.Pointer(cCachePath)) + defer C.free(unsafe.Pointer(cParentsFilename)) + defer C.free(unsafe.Pointer(cReplicaPath)) + return int(C.c1(C.size_t(blockOffset), C.size_t(numSectors), C.size_t(sectorSlot), cReplicaID, cSeed, cTicket, cCachePath, cParentsFilename, cReplicaPath, C.size_t(sectorSize))) +} + +// GetMaxBlockOffset returns the highest available block offset. +func GetMaxBlockOffset(sectorSize uint64) uint64 { + return uint64(C.get_max_block_offset(C.size_t(sectorSize))) +} + +// GetSlotSize returns the size in blocks required for the given number of sectors. +func GetSlotSize(numSectors int, sectorSize uint64) uint64 { + return uint64(C.get_slot_size(C.size_t(numSectors), C.size_t(sectorSize))) +} + +// GetCommCFromTree returns comm_c after calculating from tree file(s). Returns true on success. +func GetCommCFromTree(commC []byte, cachePath string, sectorSize uint64) bool { + cCommC := (*C.uint8_t)(unsafe.Pointer(&commC[0])) + cCachePath := C.CString(cachePath) + defer C.free(unsafe.Pointer(cCachePath)) + return bool(C.get_comm_c_from_tree(cCommC, cCachePath, C.size_t(sectorSize))) +} + +// GetCommC returns comm_c from p_aux file. Returns true on success. +func GetCommC(commC []byte, cachePath string) bool { + cCommC := (*C.uint8_t)(unsafe.Pointer(&commC[0])) + cCachePath := C.CString(cachePath) + defer C.free(unsafe.Pointer(cCachePath)) + return bool(C.get_comm_c(cCommC, cCachePath)) +} + +// SetCommC sets comm_c in the p_aux file. Returns true on success. +func SetCommC(commC []byte, cachePath string) bool { + cCommC := (*C.uint8_t)(unsafe.Pointer(&commC[0])) + cCachePath := C.CString(cachePath) + defer C.free(unsafe.Pointer(cCachePath)) + return bool(C.set_comm_c(cCommC, cCachePath)) +} + +// GetCommRLastFromTree returns comm_r_last after calculating from tree file(s). Returns true on success. +func GetCommRLastFromTree(commRLast []byte, cachePath string, sectorSize uint64) bool { + cCommRLast := (*C.uint8_t)(unsafe.Pointer(&commRLast[0])) + cCachePath := C.CString(cachePath) + defer C.free(unsafe.Pointer(cCachePath)) + return bool(C.get_comm_r_last_from_tree(cCommRLast, cCachePath, C.size_t(sectorSize))) +} + +// GetCommRLast returns comm_r_last from p_aux file. Returns true on success. +func GetCommRLast(commRLast []byte, cachePath string) bool { + cCommRLast := (*C.uint8_t)(unsafe.Pointer(&commRLast[0])) + cCachePath := C.CString(cachePath) + defer C.free(unsafe.Pointer(cCachePath)) + return bool(C.get_comm_r_last(cCommRLast, cCachePath)) +} + +// SetCommRLast sets comm_r_last in the p_aux file. +func SetCommRLast(commRLast []byte, cachePath string) bool { + cCommRLast := (*C.uint8_t)(unsafe.Pointer(&commRLast[0])) + cCachePath := C.CString(cachePath) + defer C.free(unsafe.Pointer(cCachePath)) + return bool(C.set_comm_r_last(cCommRLast, cCachePath)) +} + +// GetCommR returns comm_r after calculating from p_aux file. Returns true on success. +func GetCommR(commR []byte, cachePath string) bool { + cCommR := (*C.uint8_t)(unsafe.Pointer(&commR[0])) + cCachePath := C.CString(cachePath) + defer C.free(unsafe.Pointer(cCachePath)) + return bool(C.get_comm_r(cCommR, cCachePath)) +} + +// GetCommD returns comm_d from tree_d file. Returns true on success. +func GetCommD(commD []byte, cachePath string) bool { + cCommD := (*C.uint8_t)(unsafe.Pointer(&commD[0])) + cCachePath := C.CString(cachePath) + defer C.free(unsafe.Pointer(cCachePath)) + return bool(C.get_comm_d(cCommD, cCachePath)) +} + +// GetCCCommD returns comm_d for a cc sector. Returns true on success. +func GetCCCommD(commD []byte, sectorSize int) bool { + cCommD := (*C.uint8_t)(unsafe.Pointer(&commD[0])) + return bool(C.get_cc_comm_d(cCommD, C.size_t(sectorSize))) +} diff --git a/market/lmrpc/minerhandler.go b/market/lmrpc/minerhandler.go index 8d60bb360..8bb2ab907 100644 --- a/market/lmrpc/minerhandler.go +++ b/market/lmrpc/minerhandler.go @@ -11,7 +11,6 @@ import ( "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/lib/rpcenc" - "github.com/filecoin-project/lotus/metrics" "github.com/filecoin-project/lotus/metrics/proxy" "github.com/filecoin-project/lotus/node/impl" ) @@ -51,9 +50,6 @@ func MinerHandler(a api.StorageMiner, permissioned bool) (http.Handler, error) { m := mux.NewRouter() m.Handle("/rpc/v0", rpcServer) m.Handle("/rpc/streams/v0/push/{uuid}", readerHandler) - // debugging - m.Handle("/debug/metrics", metrics.Exporter()) - m.PathPrefix("/").Handler(http.DefaultServeMux) // pprof var hnd http.Handler = m if permissioned { diff --git a/tasks/seal/task_finalize.go b/tasks/seal/task_finalize.go index d901e1ebd..6d6847736 100644 --- a/tasks/seal/task_finalize.go +++ b/tasks/seal/task_finalize.go @@ -11,6 +11,7 @@ import ( "github.com/filecoin-project/curio/harmony/harmonytask" "github.com/filecoin-project/curio/harmony/resources" "github.com/filecoin-project/curio/lib/ffi" + "github.com/filecoin-project/curio/lib/slotmgr" "github.com/filecoin-project/lotus/storage/sealer/storiface" ) @@ -20,14 +21,19 @@ type FinalizeTask struct { sp *SealPoller sc *ffi.SealCalls db *harmonydb.DB + + // Batch, nillable! + slots *slotmgr.SlotMgr } -func NewFinalizeTask(max int, sp *SealPoller, sc *ffi.SealCalls, db *harmonydb.DB) *FinalizeTask { +func NewFinalizeTask(max int, sp *SealPoller, sc *ffi.SealCalls, db *harmonydb.DB, slots *slotmgr.SlotMgr) *FinalizeTask { return &FinalizeTask{ max: max, sp: sp, sc: sc, db: db, + + slots: slots, } } @@ -77,6 +83,52 @@ func (f *FinalizeTask) Do(taskID harmonytask.TaskID, stillOwned func() bool) (do ProofType: abi.RegisteredSealProof(task.RegSealProof), } + var ownedBy []struct { + HostAndPort string `db:"host_and_port"` + } + var refs []struct { + PipelineSlot int64 `db:"pipeline_slot"` + } + + if f.slots != nil { + // batch handling part 1: + // get machine id + + err = f.db.Select(ctx, &ownedBy, `SELECT hm.host_and_port as host_and_port FROM harmony_task INNER JOIN harmony_machines hm on harmony_task.owner_id = hm.id WHERE harmony_task.id = $1`, taskID) + if err != nil { + return false, xerrors.Errorf("getting machine id: %w", err) + } + + if len(ownedBy) != 1 { + return false, xerrors.Errorf("expected one machine") + } + + /* + CREATE TABLE batch_sector_refs ( + sp_id BIGINT NOT NULL, + sector_number BIGINT NOT NULL, + + machine_host_and_port TEXT NOT NULL, + pipeline_slot BIGINT NOT NULL, + + PRIMARY KEY (sp_id, sector_number, machine_host_and_port, pipeline_slot), + FOREIGN KEY (sp_id, sector_number) REFERENCES sectors_sdr_pipeline (sp_id, sector_number) + ); + */ + + // select the ref by sp_id and sector_number + // if we (ownedBy) are NOT the same as machine_host_and_port, then fail this finalize, it's really bad, and not our job + + err = f.db.Select(ctx, &refs, `SELECT pipeline_slot FROM batch_sector_refs WHERE sp_id = $1 AND sector_number = $2 AND machine_host_and_port = $3`, task.SpID, task.SectorNumber, ownedBy[0].HostAndPort) + if err != nil { + return false, xerrors.Errorf("getting batch refs: %w", err) + } + + if len(refs) != 1 { + return false, xerrors.Errorf("expected one batch ref") + } + } + err = f.sc.FinalizeSector(ctx, sector, keepUnsealed) if err != nil { return false, xerrors.Errorf("finalizing sector: %w", err) @@ -86,6 +138,47 @@ func (f *FinalizeTask) Do(taskID harmonytask.TaskID, stillOwned func() bool) (do return false, xerrors.Errorf("dropping sector piece refs: %w", err) } + if f.slots != nil { + // batch handling part 2: + + // delete from batch_sector_refs + var freeSlot bool + + _, err = f.db.BeginTransaction(ctx, func(tx *harmonydb.Tx) (commit bool, err error) { + _, err = tx.Exec(`DELETE FROM batch_sector_refs WHERE sp_id = $1 AND sector_number = $2`, task.SpID, task.SectorNumber) + if err != nil { + return false, xerrors.Errorf("deleting batch refs: %w", err) + } + + // get sector ref count, if zero free the pipeline slot + var count []struct { + Count int64 `db:"count"` + } + err = tx.Select(&count, `SELECT COUNT(1) as count FROM batch_sector_refs WHERE machine_host_and_port = $1 AND pipeline_slot = $2`, ownedBy[0].HostAndPort, refs[0].PipelineSlot) + if err != nil { + return false, xerrors.Errorf("getting batch ref count: %w", err) + } + + if count[0].Count == 0 { + freeSlot = true + } else { + log.Infow("Not freeing batch slot", "slot", refs[0].PipelineSlot, "machine", ownedBy[0].HostAndPort, "remaining", count[0].Count) + } + + return true, nil + }, harmonydb.OptionRetry()) + if err != nil { + return false, xerrors.Errorf("deleting batch refs: %w", err) + } + + if freeSlot { + log.Infow("Freeing batch slot", "slot", refs[0].PipelineSlot, "machine", ownedBy[0].HostAndPort) + if err := f.slots.Put(uint64(refs[0].PipelineSlot)); err != nil { + return false, xerrors.Errorf("freeing slot: %w", err) + } + } + } + // set after_finalize _, err = f.db.Exec(ctx, `UPDATE sectors_sdr_pipeline SET after_finalize = TRUE, task_id_finalize = NULL WHERE task_id_finalize = $1`, taskID) if err != nil { diff --git a/tasks/seal/task_sdr.go b/tasks/seal/task_sdr.go index 2e66a15e4..6072e50e6 100644 --- a/tasks/seal/task_sdr.go +++ b/tasks/seal/task_sdr.go @@ -45,16 +45,17 @@ type SDRTask struct { sc *ffi2.SealCalls - max int + max, min int } -func NewSDRTask(api SDRAPI, db *harmonydb.DB, sp *SealPoller, sc *ffi2.SealCalls, maxSDR int) *SDRTask { +func NewSDRTask(api SDRAPI, db *harmonydb.DB, sp *SealPoller, sc *ffi2.SealCalls, maxSDR, minSDR int) *SDRTask { return &SDRTask{ api: api, db: db, sp: sp, sc: sc, max: maxSDR, + min: minSDR, } } @@ -101,7 +102,7 @@ func (s *SDRTask) Do(taskID harmonytask.TaskID, stillOwned func() bool) (done bo // FAIL: api may be down // FAIL-RESP: rely on harmony retry - ticket, ticketEpoch, err := s.getTicket(ctx, maddr) + ticket, ticketEpoch, err := GetTicket(ctx, s.api, maddr) if err != nil { return false, xerrors.Errorf("getting ticket: %w", err) } @@ -136,8 +137,13 @@ func (s *SDRTask) Do(taskID harmonytask.TaskID, stillOwned func() bool) (done bo return true, nil } -func (s *SDRTask) getTicket(ctx context.Context, maddr address.Address) (abi.SealRandomness, abi.ChainEpoch, error) { - ts, err := s.api.ChainHead(ctx) +type TicketNodeAPI interface { + ChainHead(context.Context) (*types.TipSet, error) + StateGetRandomnessFromTickets(context.Context, crypto.DomainSeparationTag, abi.ChainEpoch, []byte, types.TipSetKey) (abi.Randomness, error) +} + +func GetTicket(ctx context.Context, api TicketNodeAPI, maddr address.Address) (abi.SealRandomness, abi.ChainEpoch, error) { + ts, err := api.ChainHead(ctx) if err != nil { return nil, 0, xerrors.Errorf("getting chain head: %w", err) } @@ -148,7 +154,7 @@ func (s *SDRTask) getTicket(ctx context.Context, maddr address.Address) (abi.Sea return nil, 0, xerrors.Errorf("marshaling miner address: %w", err) } - rand, err := s.api.StateGetRandomnessFromTickets(ctx, crypto.DomainSeparationTag_SealRandomness, ticketEpoch, buf.Bytes(), ts.Key()) + rand, err := api.StateGetRandomnessFromTickets(ctx, crypto.DomainSeparationTag_SealRandomness, ticketEpoch, buf.Bytes(), ts.Key()) if err != nil { return nil, 0, xerrors.Errorf("getting randomness from tickets: %w", err) } @@ -157,6 +163,11 @@ func (s *SDRTask) getTicket(ctx context.Context, maddr address.Address) (abi.Sea } func (s *SDRTask) CanAccept(ids []harmonytask.TaskID, engine *harmonytask.TaskEngine) (*harmonytask.TaskID, error) { + if s.min > len(ids) { + log.Debugw("did not accept task", "name", "SDR", "reason", "below min", "min", s.min, "count", len(ids)) + return nil, nil + } + id := ids[0] return &id, nil } diff --git a/tasks/seal/task_submit_precommit.go b/tasks/seal/task_submit_precommit.go index fdd28f53f..526d51c0d 100644 --- a/tasks/seal/task_submit_precommit.go +++ b/tasks/seal/task_submit_precommit.go @@ -81,16 +81,17 @@ func (s *SubmitPrecommitTask) Do(taskID harmonytask.TaskID, stillOwned func() bo // 1. Load sector info var sectorParamsArr []struct { - SpID int64 `db:"sp_id"` - SectorNumber int64 `db:"sector_number"` - RegSealProof abi.RegisteredSealProof `db:"reg_seal_proof"` - TicketEpoch abi.ChainEpoch `db:"ticket_epoch"` - SealedCID string `db:"tree_r_cid"` - UnsealedCID string `db:"tree_d_cid"` + SpID int64 `db:"sp_id"` + SectorNumber int64 `db:"sector_number"` + RegSealProof abi.RegisteredSealProof `db:"reg_seal_proof"` + UserSectorDurationEpochs *int64 `db:"user_sector_duration_epochs"` + TicketEpoch abi.ChainEpoch `db:"ticket_epoch"` + SealedCID string `db:"tree_r_cid"` + UnsealedCID string `db:"tree_d_cid"` } err = s.db.Select(ctx, §orParamsArr, ` - SELECT sp_id, sector_number, reg_seal_proof, ticket_epoch, tree_r_cid, tree_d_cid + SELECT sp_id, sector_number, reg_seal_proof, user_sector_duration_epochs, ticket_epoch, tree_r_cid, tree_d_cid FROM sectors_sdr_pipeline WHERE task_id_precommit_msg = $1`, taskID) if err != nil { diff --git a/tasks/sealsupra/metrics.go b/tasks/sealsupra/metrics.go new file mode 100644 index 000000000..967ee5bbb --- /dev/null +++ b/tasks/sealsupra/metrics.go @@ -0,0 +1,47 @@ +package sealsupra + +import ( + "go.opencensus.io/stats" + "go.opencensus.io/stats/view" + "go.opencensus.io/tag" +) + +var ( + phaseKey, _ = tag.NewKey("phase") + pre = "sealsupra_" +) + +// SupraSealMeasures groups all SupraSeal metrics. +var SupraSealMeasures = struct { + PhaseLockCount *stats.Int64Measure + PhaseWaitingCount *stats.Int64Measure + PhaseAvgDuration *stats.Float64Measure +}{ + PhaseLockCount: stats.Int64(pre+"phase_lock_count", "Number of active locks in each phase", stats.UnitDimensionless), + PhaseWaitingCount: stats.Int64(pre+"phase_waiting_count", "Number of goroutines waiting for a phase lock", stats.UnitDimensionless), + PhaseAvgDuration: stats.Float64(pre+"phase_avg_duration", "Average duration of each phase in seconds", stats.UnitSeconds), +} + +// init registers the views for SupraSeal metrics. +func init() { + err := view.Register( + &view.View{ + Measure: SupraSealMeasures.PhaseLockCount, + Aggregation: view.LastValue(), + TagKeys: []tag.Key{phaseKey}, + }, + &view.View{ + Measure: SupraSealMeasures.PhaseWaitingCount, + Aggregation: view.LastValue(), + TagKeys: []tag.Key{phaseKey}, + }, + &view.View{ + Measure: SupraSealMeasures.PhaseAvgDuration, + Aggregation: view.LastValue(), + TagKeys: []tag.Key{phaseKey}, + }, + ) + if err != nil { + panic(err) + } +} diff --git a/tasks/sealsupra/phase.go b/tasks/sealsupra/phase.go new file mode 100644 index 000000000..d2acbe3b9 --- /dev/null +++ b/tasks/sealsupra/phase.go @@ -0,0 +1,63 @@ +package sealsupra + +import ( + "context" + "fmt" + "sync" + "sync/atomic" + "time" + + "go.opencensus.io/stats" + "go.opencensus.io/tag" +) + +const alpha = 0.4 // EMA smoothing factor + +// pipelinePhase ensures that there is only one pipeline in each phase +// could be a simple lock, but this gives us some stats +type pipelinePhase struct { + phaseLock sync.Mutex + phaseNum int + active int64 + waiting int64 + ema float64 // Exponential Moving Average in seconds + lastLockAt time.Time +} + +func (p *pipelinePhase) Lock() { + atomic.AddInt64(&p.waiting, 1) + _ = stats.RecordWithTags(context.Background(), + []tag.Mutator{tag.Upsert(phaseKey, fmt.Sprintf("phase_%d", p.phaseNum))}, + SupraSealMeasures.PhaseWaitingCount.M(atomic.LoadInt64(&p.waiting))) + + p.phaseLock.Lock() + + atomic.AddInt64(&p.waiting, -1) + atomic.AddInt64(&p.active, 1) + _ = stats.RecordWithTags(context.Background(), + []tag.Mutator{tag.Upsert(phaseKey, fmt.Sprintf("phase_%d", p.phaseNum))}, + SupraSealMeasures.PhaseLockCount.M(atomic.LoadInt64(&p.active)), + SupraSealMeasures.PhaseWaitingCount.M(atomic.LoadInt64(&p.waiting))) + + p.lastLockAt = time.Now() +} + +func (p *pipelinePhase) Unlock() { + duration := time.Since(p.lastLockAt) + durationSeconds := duration.Seconds() + + // Update EMA + if p.ema == 0 { + p.ema = durationSeconds // Initialize EMA with first value + } else { + p.ema = alpha*durationSeconds + (1-alpha)*p.ema + } + + atomic.AddInt64(&p.active, -1) + _ = stats.RecordWithTags(context.Background(), + []tag.Mutator{tag.Upsert(phaseKey, fmt.Sprintf("phase_%d", p.phaseNum))}, + SupraSealMeasures.PhaseLockCount.M(atomic.LoadInt64(&p.active)), + SupraSealMeasures.PhaseAvgDuration.M(p.ema)) + + p.phaseLock.Unlock() +} diff --git a/tasks/sealsupra/supra_config.go b/tasks/sealsupra/supra_config.go new file mode 100644 index 000000000..39105b9d6 --- /dev/null +++ b/tasks/sealsupra/supra_config.go @@ -0,0 +1,426 @@ +package sealsupra + +import ( + "bufio" + "fmt" + "os/exec" + "regexp" + "strconv" + "strings" + + "github.com/samber/lo" +) + +type SystemInfo struct { + ProcessorCount int + CoreCount int + ThreadsPerCore int + CoresPerL3 int +} + +type CoordinatorConfig struct { + Core int + Hashers int +} + +type SectorConfig struct { + Sectors int + Coordinators []CoordinatorConfig +} + +type TopologyConfig struct { + PC1Writer int + PC1Reader int + PC1Orchestrator int + PC2Reader int + PC2Hasher int + PC2HasherCPU int + PC2Writer int + PC2WriterCores int + C1Reader int + SectorConfigs []SectorConfig +} + +type SupraSealConfig struct { + NVMeDevices []string + Topology TopologyConfig + + // Diagnostic fields (not part of the config) + RequiredThreads int + RequiredCCX int + RequiredCores int + UnoccupiedCores int + + P2WrRdOverlap bool + P2HsP1WrOverlap bool + P2HcP2RdOverlap bool +} + +type AdditionalSystemInfo struct { + CPUName string + MemorySize string + MemoryChannels int + InstalledModules int + MaxMemoryCapacity string + MemoryType string + MemorySpeed string +} + +func GetSystemInfo() (*SystemInfo, error) { + cmd := exec.Command("hwloc-ls") + output, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("error running hwloc-ls: %v", err) + } + + info := &SystemInfo{} + scanner := bufio.NewScanner(strings.NewReader(string(output))) + + l3Regex := regexp.MustCompile(`L3 L#(\d+)`) + puRegex := regexp.MustCompile(`PU L#(\d+)`) + coreRegex := regexp.MustCompile(`Core L#(\d+)`) + packageRegex := regexp.MustCompile(`Package L#(\d+)`) + + var currentL3Cores int + var lastL3Index int = -1 + var threadCount int + + for scanner.Scan() { + line := scanner.Text() + + if packageRegex.MatchString(line) { + info.ProcessorCount++ + } + + if info.ProcessorCount > 1 { + // in multi-socket systems, we only care about the first socket, rest are the same + continue + } + + if l3Match := l3Regex.FindStringSubmatch(line); l3Match != nil { + l3Index, _ := strconv.Atoi(l3Match[1]) + if lastL3Index != -1 && l3Index != lastL3Index { + if info.CoresPerL3 == 0 || currentL3Cores < info.CoresPerL3 { + info.CoresPerL3 = currentL3Cores + } + currentL3Cores = 0 + } + lastL3Index = l3Index + } + + if coreRegex.MatchString(line) { + info.CoreCount++ + currentL3Cores++ + } + + if puRegex.MatchString(line) { + threadCount++ + } + } + + // Handle the last L3 cache + if info.CoresPerL3 == 0 || currentL3Cores < info.CoresPerL3 { + info.CoresPerL3 = currentL3Cores + } + + if info.CoreCount > 0 { + info.ThreadsPerCore = threadCount / info.CoreCount + } + + return info, nil +} + +func GenerateSupraSealConfig(info SystemInfo, dualHashers bool, batchSize int, nvmeDevices []string) (SupraSealConfig, error) { + config := SupraSealConfig{ + NVMeDevices: nvmeDevices, + Topology: TopologyConfig{ + // Start with a somewhat optimal layout for top-level P1 processes + PC1Writer: 1, + PC1Reader: 2, // High load + PC1Orchestrator: 3, // High load + + // Now cram P2 processes into the remaining ~2 cores + PC2Reader: 0, + PC2Hasher: 1, // High load + PC2HasherCPU: 0, + PC2Writer: 0, // High load + PC2WriterCores: 1, // ^ + C1Reader: 0, + }, + + P2WrRdOverlap: true, + P2HsP1WrOverlap: true, + P2HcP2RdOverlap: true, + } + + sectorsPerThread := 1 + if dualHashers { + sectorsPerThread = 2 + } + + ccxFreeCores := info.CoresPerL3 - 1 // one core per ccx goes to the coordinator + ccxFreeThreads := ccxFreeCores * info.ThreadsPerCore + sectorsPerCCX := ccxFreeThreads * sectorsPerThread + + config.RequiredThreads = batchSize / sectorsPerThread + config.RequiredCCX = (batchSize + sectorsPerCCX - 1) / sectorsPerCCX + config.RequiredCores = config.RequiredCCX + config.RequiredThreads/info.ThreadsPerCore + + if config.RequiredCores > info.CoreCount { + return config, fmt.Errorf("not enough cores available for hashers") + } + + coresLeftover := info.CoreCount - config.RequiredCores + + const minOverheadCores = 4 + if coresLeftover < minOverheadCores { + return config, fmt.Errorf("not enough cores available for overhead") + } + + nextFreeCore := minOverheadCores + + // Assign cores for PC2 processes + if coresLeftover > nextFreeCore { + config.Topology.PC2Writer = nextFreeCore + config.P2WrRdOverlap = false + nextFreeCore++ + } + + if coresLeftover > nextFreeCore { + config.Topology.PC2Hasher = nextFreeCore + config.P2HsP1WrOverlap = false + nextFreeCore++ + } + + if coresLeftover > nextFreeCore { + config.Topology.PC2HasherCPU = nextFreeCore + config.P2HcP2RdOverlap = false + nextFreeCore++ + } + + if coresLeftover > nextFreeCore { + config.Topology.PC2Reader = nextFreeCore + config.Topology.C1Reader = nextFreeCore + nextFreeCore++ + } + + // Add P2 writer cores, up to 8 total + if coresLeftover > nextFreeCore { + config.Topology.PC2Writer, config.Topology.PC2Reader = config.Topology.PC2Reader, config.Topology.PC2Writer + + for i := 0; i < 7 && coresLeftover > nextFreeCore; i++ { + config.Topology.PC2WriterCores++ + nextFreeCore++ + } + } + + config.UnoccupiedCores = coresLeftover - nextFreeCore + + sectorConfig := SectorConfig{ + Sectors: batchSize, + Coordinators: []CoordinatorConfig{}, + } + + ccxCores := make([]int, 0) + for i := 0; i < info.CoreCount; i += info.CoresPerL3 { + ccxCores = append(ccxCores, i) + } + + for i := config.RequiredCores; i > 0; { + firstCCXCoreNum := ccxCores[len(ccxCores)-1] + toAssign := min(i, info.CoresPerL3) + + coreNum := firstCCXCoreNum + info.CoresPerL3 - toAssign + + sectorConfig.Coordinators = append(sectorConfig.Coordinators, CoordinatorConfig{ + Core: coreNum, + Hashers: (toAssign - 1) * info.ThreadsPerCore, + }) + + i -= toAssign + if toAssign == info.CoresPerL3 { + ccxCores = ccxCores[:len(ccxCores)-1] + if len(ccxCores) == 0 { + break + } + } + } + + // Reverse the order of coordinators + for i, j := 0, len(sectorConfig.Coordinators)-1; i < j; i, j = i+1, j-1 { + sectorConfig.Coordinators[i], sectorConfig.Coordinators[j] = sectorConfig.Coordinators[j], sectorConfig.Coordinators[i] + } + + config.Topology.SectorConfigs = append(config.Topology.SectorConfigs, sectorConfig) + + return config, nil +} + +func FormatSupraSealConfig(config SupraSealConfig, system SystemInfo, additionalInfo AdditionalSystemInfo) string { + var sb strings.Builder + + w := func(s string) { sb.WriteString(s); sb.WriteByte('\n') } + + w("# Configuration for supra_seal") + w("") + w("# Machine Specifications:") + w(fmt.Sprintf("# CPU: %s", additionalInfo.CPUName)) + w(fmt.Sprintf("# Memory: %s", additionalInfo.MemorySize)) + w(fmt.Sprintf("# Memory Type: %s", additionalInfo.MemoryType)) + w(fmt.Sprintf("# Memory Speed: %s", additionalInfo.MemorySpeed)) + w(fmt.Sprintf("# Installed Memory Modules: %d", additionalInfo.InstalledModules)) + w(fmt.Sprintf("# Maximum Memory Capacity: %s", additionalInfo.MaxMemoryCapacity)) + w(fmt.Sprintf("# Memory Channels: %d", additionalInfo.MemoryChannels)) + w(fmt.Sprintf("# Processor Count: %d", system.ProcessorCount)) + w(fmt.Sprintf("# Core Count: %d", system.CoreCount)) + w(fmt.Sprintf("# Threads per Core: %d", system.ThreadsPerCore)) + w(fmt.Sprintf("# Cores per L3 Cache: %d", system.CoresPerL3)) + w("") + w("# Diagnostic Information:") + w(fmt.Sprintf("# Required Threads: %d", config.RequiredThreads)) + w(fmt.Sprintf("# Required CCX: %d", config.RequiredCCX)) + w(fmt.Sprintf("# Required Cores: %d", config.RequiredCores)) + w(fmt.Sprintf("# Unoccupied Cores: %d", config.UnoccupiedCores)) + w(fmt.Sprintf("# P2 Writer/Reader Overlap: %v", config.P2WrRdOverlap)) + w(fmt.Sprintf("# P2 Hasher/P1 Writer Overlap: %v", config.P2HsP1WrOverlap)) + w(fmt.Sprintf("# P2 Hasher CPU/P2 Reader Overlap: %v", config.P2HcP2RdOverlap)) + w("") + w("spdk: {") + w(" # PCIe identifiers of NVMe drives to use to store layers") + w(" nvme = [ ") + + quotedNvme := lo.Map(config.NVMeDevices, func(d string, _ int) string { return ` "` + d + `"` }) + w(strings.Join(quotedNvme, ",\n")) + + w(" ];") + w("}") + w("") + w("# CPU topology for various parallel sector counts") + w("topology:") + w("{") + w(" pc1: {") + w(fmt.Sprintf(" writer = %d;", config.Topology.PC1Writer)) + w(fmt.Sprintf(" reader = %d;", config.Topology.PC1Reader)) + w(fmt.Sprintf(" orchestrator = %d;", config.Topology.PC1Orchestrator)) + w(" qpair_reader = 0;") + w(" qpair_writer = 1;") + w(" reader_sleep_time = 250;") + w(" writer_sleep_time = 500;") + w(" hashers_per_core = 2;") + w("") + w(" sector_configs: (") + + sectorConfigsStr := lo.Map(config.Topology.SectorConfigs, func(sectorConfig SectorConfig, i int) string { + coordsStr := lo.Map(sectorConfig.Coordinators, func(coord CoordinatorConfig, j int) string { + return fmt.Sprintf(" { core = %d;\n hashers = %d; }%s\n", + coord.Core, coord.Hashers, lo.Ternary(j < len(sectorConfig.Coordinators)-1, ",", "")) + }) + + return fmt.Sprintf(" {\n sectors = %d;\n coordinators = (\n%s )\n }%s\n", + sectorConfig.Sectors, strings.Join(coordsStr, ""), lo.Ternary(i < len(config.Topology.SectorConfigs)-1, ",", "")) + }) + + w(strings.Join(sectorConfigsStr, "")) + + w(" )") + w(" },") + w("") + w(" pc2: {") + w(fmt.Sprintf(" reader = %d;", config.Topology.PC2Reader)) + w(fmt.Sprintf(" hasher = %d;", config.Topology.PC2Hasher)) + w(fmt.Sprintf(" hasher_cpu = %d;", config.Topology.PC2HasherCPU)) + w(fmt.Sprintf(" writer = %d;", config.Topology.PC2Writer)) + w(fmt.Sprintf(" writer_cores = %d;", config.Topology.PC2WriterCores)) + w(" sleep_time = 200;") + w(" qpair = 2;") + w(" },") + w("") + w(" c1: {") + w(fmt.Sprintf(" reader = %d;", config.Topology.C1Reader)) + w(" sleep_time = 200;") + w(" qpair = 3;") + w(" }") + w("}") + + return sb.String() +} + +func ExtractAdditionalSystemInfo() (AdditionalSystemInfo, error) { + info := AdditionalSystemInfo{} + + // Extract CPU Name (unchanged) + cpuInfoCmd := exec.Command("lscpu") + cpuInfoOutput, err := cpuInfoCmd.Output() + if err != nil { + return info, fmt.Errorf("failed to execute lscpu: %v", err) + } + + cpuInfoLines := strings.Split(string(cpuInfoOutput), "\n") + for _, line := range cpuInfoLines { + if strings.HasPrefix(line, "Model name:") { + info.CPUName = strings.TrimSpace(strings.TrimPrefix(line, "Model name:")) + break + } + } + + // Extract Memory Information + memInfoCmd := exec.Command("dmidecode", "-t", "memory") + memInfoOutput, err := memInfoCmd.Output() + if err != nil { + log.Warnf("failed to execute dmidecode: %v", err) + return info, nil + } + + memInfoLines := strings.Split(string(memInfoOutput), "\n") + var totalMemory int64 + for _, line := range memInfoLines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "Maximum Capacity:") { + info.MaxMemoryCapacity = strings.TrimSpace(strings.TrimPrefix(line, "Maximum Capacity:")) + } else if strings.HasPrefix(line, "Number Of Devices:") { + info.MemoryChannels, _ = strconv.Atoi(strings.TrimSpace(strings.TrimPrefix(line, "Number Of Devices:"))) + } else if strings.HasPrefix(line, "Size:") { + if strings.Contains(line, "GB") { + sizeStr := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(line, "Size:"), "GB")) + size, _ := strconv.ParseInt(sizeStr, 10, 64) + if size > 0 { + totalMemory += size + info.InstalledModules++ + } + } + } else if strings.HasPrefix(line, "Type:") && info.MemoryType == "" { + info.MemoryType = strings.TrimSpace(strings.TrimPrefix(line, "Type:")) + } else if strings.HasPrefix(line, "Speed:") && info.MemorySpeed == "" { + info.MemorySpeed = strings.TrimSpace(strings.TrimPrefix(line, "Speed:")) + } + } + + info.MemorySize = fmt.Sprintf("%d GB", totalMemory) + + return info, nil +} + +func GenerateSupraSealConfigString(dualHashers bool, batchSize int, nvmeDevices []string) (string, error) { + // Get system information + sysInfo, err := GetSystemInfo() + if err != nil { + return "", fmt.Errorf("failed to get system info: %v", err) + } + + // Generate SupraSealConfig + config, err := GenerateSupraSealConfig(*sysInfo, dualHashers, batchSize, nvmeDevices) + if err != nil { + return "", fmt.Errorf("failed to generate SupraSeal config: %v", err) + } + + // Get additional system information + additionalInfo, err := ExtractAdditionalSystemInfo() + if err != nil { + return "", fmt.Errorf("failed to extract additional system info: %v", err) + } + + // Format the config + configString := FormatSupraSealConfig(config, *sysInfo, additionalInfo) + + return configString, nil +} diff --git a/tasks/sealsupra/task_supraseal.go b/tasks/sealsupra/task_supraseal.go new file mode 100644 index 000000000..9bd6997af --- /dev/null +++ b/tasks/sealsupra/task_supraseal.go @@ -0,0 +1,529 @@ +package sealsupra + +import ( + "context" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "path/filepath" + "time" + + logging "github.com/ipfs/go-log/v2" + "github.com/snadrus/must" + "golang.org/x/xerrors" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-commp-utils/zerocomm" + commcid "github.com/filecoin-project/go-fil-commcid" + "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/crypto" + + "github.com/filecoin-project/curio/harmony/harmonydb" + "github.com/filecoin-project/curio/harmony/harmonytask" + "github.com/filecoin-project/curio/harmony/resources" + "github.com/filecoin-project/curio/lib/hugepageutil" + "github.com/filecoin-project/curio/lib/passcall" + "github.com/filecoin-project/curio/lib/paths" + "github.com/filecoin-project/curio/lib/slotmgr" + "github.com/filecoin-project/curio/lib/supraffi" + "github.com/filecoin-project/curio/tasks/seal" + + "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/lotus/storage/sealer/storiface" +) + +const suprasealConfigEnv = "SUPRASEAL_CONFIG" + +var log = logging.Logger("batchseal") + +type SupraSealNodeAPI interface { + ChainHead(context.Context) (*types.TipSet, error) + StateGetRandomnessFromTickets(context.Context, crypto.DomainSeparationTag, abi.ChainEpoch, []byte, types.TipSetKey) (abi.Randomness, error) +} + +type SupraSeal struct { + db *harmonydb.DB + api SupraSealNodeAPI + storage *paths.Remote + sindex paths.SectorIndex + + pipelines int // 1 or 2 + sectors int // sectors in a batch + spt abi.RegisteredSealProof + + inSDR *pipelinePhase // Phase 1 + outSDR *pipelinePhase // Phase 2 + + slots *slotmgr.SlotMgr +} + +func NewSupraSeal(sectorSize string, batchSize, pipelines int, dualHashers bool, nvmeDevices []string, machineHostAndPort string, + slots *slotmgr.SlotMgr, db *harmonydb.DB, api SupraSealNodeAPI, storage *paths.Remote, sindex paths.SectorIndex) (*SupraSeal, error) { + var spt abi.RegisteredSealProof + switch sectorSize { + case "32GiB": + spt = abi.RegisteredSealProof_StackedDrg32GiBV1_1 + default: + return nil, xerrors.Errorf("unsupported sector size: %s", sectorSize) + } + + ssize, err := spt.SectorSize() + if err != nil { + return nil, err + } + + log.Infow("start supraseal init") + var configFile string + if configFile = os.Getenv(suprasealConfigEnv); configFile == "" { + // not set from env (should be the case in most cases), auto-generate a config + + cstr, err := GenerateSupraSealConfigString(dualHashers, batchSize, nvmeDevices) + if err != nil { + return nil, xerrors.Errorf("generating supraseal config: %w", err) + } + + cfgFile, err := os.CreateTemp("", "supraseal-config-*.cfg") + if err != nil { + return nil, xerrors.Errorf("creating temp file: %w", err) + } + + if _, err := cfgFile.WriteString(cstr); err != nil { + return nil, xerrors.Errorf("writing temp file: %w", err) + } + + configFile = cfgFile.Name() + if err := cfgFile.Close(); err != nil { + return nil, xerrors.Errorf("closing temp file: %w", err) + } + + log.Infow("generated supraseal config", "config", cstr, "file", configFile) + } + + supraffi.SupraSealInit(uint64(ssize), configFile) + log.Infow("supraseal init done") + + // Get maximum block offset (essentially the number of pages in the smallest nvme device) + space := supraffi.GetMaxBlockOffset(uint64(ssize)) + + // Get slot size (number of pages per device used for 11 layers * sector count) + slotSize := supraffi.GetSlotSize(batchSize, uint64(ssize)) + + maxPipelines := space / slotSize + if maxPipelines < uint64(pipelines) { + return nil, xerrors.Errorf("not enough space for %d pipelines (can do %d), only %d pages available, want %d (slot size %d) pages", pipelines, maxPipelines, space, slotSize*uint64(pipelines), slotSize) + } + + for i := 0; i < pipelines; i++ { + slot := slotSize * uint64(i) + + var slotRefs []struct { + Count int `db:"count"` + } + + err := db.Select(context.Background(), &slotRefs, `SELECT COUNT(*) as count FROM batch_sector_refs WHERE pipeline_slot = $1 AND machine_host_and_port = $2`, slot, machineHostAndPort) + if err != nil { + return nil, xerrors.Errorf("getting slot refs: %w", err) + } + + if len(slotRefs) > 0 { + if slotRefs[0].Count > 0 { + log.Infow("slot already in use", "slot", slot, "refs", slotRefs[0].Count) + continue + } + } + + log.Infow("batch slot", "slot", slot, "machine", machineHostAndPort) + + err = slots.Put(slot) + if err != nil { + return nil, xerrors.Errorf("putting slot: %w", err) + } + } + + return &SupraSeal{ + db: db, + api: api, + storage: storage, + sindex: sindex, + + spt: spt, + pipelines: pipelines, + sectors: batchSize, + + inSDR: &pipelinePhase{phaseNum: 1}, + outSDR: &pipelinePhase{phaseNum: 2}, + + slots: slots, + }, nil +} + +func (s *SupraSeal) Do(taskID harmonytask.TaskID, stillOwned func() bool) (done bool, err error) { + ctx := context.Background() + + var sectors []struct { + SpID int64 `db:"sp_id"` + SectorNumber int64 `db:"sector_number"` + + RegSealProof int64 `db:"reg_seal_proof"` + } + + err = s.db.Select(ctx, §ors, `SELECT sp_id, sector_number, reg_seal_proof FROM sectors_sdr_pipeline WHERE task_id_sdr = $1 AND task_id_tree_r = $1 AND task_id_tree_c = $1 AND task_id_tree_d = $1`, taskID) + if err != nil { + return false, xerrors.Errorf("getting sector params: %w", err) + } + + if len(sectors) != s.sectors { + return false, xerrors.Errorf("not enough sectors to fill a batch") + } + + ssize, err := s.spt.SectorSize() + if err != nil { + return false, err + } + + unsealedCID := zerocomm.ZeroPieceCommitment(abi.PaddedPieceSize(ssize).Unpadded()) + commd, err := commcid.CIDToDataCommitmentV1(unsealedCID) + if err != nil { + return false, xerrors.Errorf("getting commd: %w", err) + } + + ticketEpochs := make([]abi.ChainEpoch, len(sectors)) + tickets := make([]abi.SealRandomness, len(sectors)) + replicaIDs := make([][32]byte, len(sectors)) + outPaths := make([]supraffi.Path, len(sectors)) + outPathIDs := make([]storiface.SectorPaths, len(sectors)) + alloc := storiface.FTSealed | storiface.FTCache + + for i, t := range sectors { + sid := abi.SectorID{ + Miner: abi.ActorID(t.SpID), + Number: abi.SectorNumber(t.SectorNumber), + } + + // cleanup any potential previous failed attempts + if err := s.storage.Remove(ctx, sid, storiface.FTSealed, true, nil); err != nil { + return false, xerrors.Errorf("removing sector: %w", err) + } + if err := s.storage.Remove(ctx, sid, storiface.FTCache, true, nil); err != nil { + return false, xerrors.Errorf("removing sector: %w", err) + } + + // get ticket + maddr, err := address.NewIDAddress(uint64(t.SpID)) + if err != nil { + return false, xerrors.Errorf("getting miner address: %w", err) + } + + ticket, ticketEpoch, err := seal.GetTicket(ctx, s.api, maddr) + if err != nil { + return false, xerrors.Errorf("getting ticket: %w", err) + } + ticketEpochs[i] = ticketEpoch + tickets[i] = ticket + + spt := abi.RegisteredSealProof(t.RegSealProof) + replicaIDs[i], err = spt.ReplicaId(abi.ActorID(t.SpID), abi.SectorNumber(t.SectorNumber), ticket, commd) + if err != nil { + return false, xerrors.Errorf("getting replica id: %w", err) + } + + // get output paths (before SDR so that allocating can fail early) + sref := storiface.SectorRef{ + ID: abi.SectorID{Miner: abi.ActorID(t.SpID), Number: abi.SectorNumber(t.SectorNumber)}, + ProofType: abi.RegisteredSealProof(t.RegSealProof), + } + + ctx := context.WithValue(ctx, paths.SpaceUseKey, paths.SpaceUseFunc(SupraSpaceUse)) + + ps, pathIDs, err := s.storage.AcquireSector(ctx, sref, storiface.FTNone, alloc, storiface.PathSealing, storiface.AcquireMove) + if err != nil { + return false, xerrors.Errorf("acquiring sector storage: %w", err) + } + + outPaths[i] = supraffi.Path{ + Replica: ps.Sealed, + Cache: ps.Cache, + } + outPathIDs[i] = pathIDs + } + + s.inSDR.Lock() + slot := s.slots.Get() + + cleanup := func() { + perr := s.slots.Put(slot) + if perr != nil { + log.Errorf("putting slot back: %s", err) + } + s.inSDR.Unlock() + } + defer func() { + cleanup() + }() + + parentPath, err := paths.ParentsForProof(s.spt) + if err != nil { + return false, xerrors.Errorf("getting parent path: %w", err) + } + + start := time.Now() //nolint:staticcheck + res := supraffi.Pc1(slot, replicaIDs, parentPath, uint64(ssize)) + duration := time.Since(start).Truncate(time.Second) + log.Infow("batch sdr done", "duration", duration, "slot", slot, "res", res, "task", taskID, "sectors", sectors, "spt", sectors[0].RegSealProof, "replicaIDs", replicaIDs) + + if res != 0 { + return false, xerrors.Errorf("pc1 failed: %d", res) + } + + s.inSDR.Unlock() + s.outSDR.Lock() + cleanup = func() { + perr := s.slots.Put(slot) + if perr != nil { + log.Errorf("putting slot back: %s", err) + } + s.outSDR.Unlock() + + // Remove any files in outPaths + for _, p := range outPaths { + if err := os.Remove(p.Replica); err != nil { + log.Errorf("removing replica file: %s", err) + } + if err := os.RemoveAll(p.Cache); err != nil { + log.Errorf("removing cache file: %s", err) + } + } + } + + log.Infow("batch tree start", "slot", slot, "task", taskID, "sectors", sectors, "pstring", hex.EncodeToString([]byte(must.One(supraffi.GenerateMultiString(outPaths))))) + + start2 := time.Now() + res = supraffi.Pc2(slot, s.sectors, must.One(supraffi.GenerateMultiString(outPaths)), uint64(ssize)) + log.Infow("batch tree done", "duration", time.Since(start2).Truncate(time.Second), "slot", slot, "res", res, "task", taskID, "sectors", sectors) + if res != 0 { + return false, xerrors.Errorf("pc2 failed: %d", res) + } + + for i, p := range outPaths { + // in each path, write a file indicating that this is a supra-sealed sector, pipeline and slot number + bmeta := paths.BatchMeta{ + SupraSeal: true, + BlockOffset: slot, + NumInPipeline: i, + + BatchSectors: s.sectors, + } + + meta, err := json.Marshal(bmeta) + if err != nil { + return false, xerrors.Errorf("marshaling meta: %w", err) + } + + if err := os.WriteFile(filepath.Join(p.Cache, paths.BatchMetaFile), meta, 0644); err != nil { + return false, xerrors.Errorf("writing meta: %w", err) + } + } + + // declare sectors + for i, ids := range outPathIDs { + sid := abi.SectorID{ + Miner: abi.ActorID(sectors[i].SpID), + Number: abi.SectorNumber(sectors[i].SectorNumber), + } + for _, ft := range alloc.AllSet() { + storageID := storiface.PathByType(ids, ft) + if err := s.sindex.StorageDeclareSector(ctx, storiface.ID(storageID), sid, ft, true); err != nil { + log.Errorf("declare sector error: %+v", err) + } + } + } + + if !stillOwned() { + return false, xerrors.Errorf("task is no longer owned!") + } + + // persist success + _, err = s.db.BeginTransaction(ctx, func(tx *harmonydb.Tx) (commit bool, err error) { + // get machine id + var ownedBy []struct { + HostAndPort string `db:"host_and_port"` + } + + err = tx.Select(&ownedBy, `SELECT hm.host_and_port FROM harmony_task INNER JOIN harmony_machines hm on harmony_task.owner_id = hm.id WHERE harmony_task.id = $1`, taskID) + if err != nil { + return false, xerrors.Errorf("getting machine id: %w", err) + } + + if len(ownedBy) != 1 { + return false, xerrors.Errorf("no machine found for task %d", taskID) + } + + for i, sector := range sectors { + var commr [32]byte + if !supraffi.GetCommR(commr[:], outPaths[i].Cache) { + return false, xerrors.Errorf("getting commr failed") + } + + sealedCID, err := commcid.ReplicaCommitmentV1ToCID(commr[:]) + if err != nil { + return false, xerrors.Errorf("getting sealed CID: %w", err) + } + + _, err = tx.Exec(`UPDATE sectors_sdr_pipeline SET after_sdr = TRUE, after_tree_c = TRUE, after_tree_r = TRUE, after_tree_d = TRUE, after_synth = TRUE, + ticket_epoch = $3, ticket_value = $4, tree_d_cid = $5, tree_r_cid = $6, task_id_sdr = NULL, task_id_tree_r = NULL, task_id_tree_c = NULL, task_id_tree_d = NULL + WHERE sp_id = $1 AND sector_number = $2`, sector.SpID, sector.SectorNumber, ticketEpochs[i], tickets[i], unsealedCID.String(), sealedCID) + if err != nil { + return false, xerrors.Errorf("updating sector: %w", err) + } + + // insert batch refs + _, err = tx.Exec(`INSERT INTO batch_sector_refs (sp_id, sector_number, machine_host_and_port, pipeline_slot) + VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING`, sector.SpID, sector.SectorNumber, ownedBy[0].HostAndPort, slot) + if err != nil { + return false, xerrors.Errorf("inserting batch refs: %w", err) + } + } + + return true, nil + }, harmonydb.OptionRetry()) + if err != nil { + return false, xerrors.Errorf("persisting success: %w", err) + } + + cleanup = func() { + s.outSDR.Unlock() + // NOTE: We're not releasing the slot yet, we keep it until sector Finalize + } + + return true, nil +} + +func (s *SupraSeal) CanAccept(ids []harmonytask.TaskID, engine *harmonytask.TaskEngine) (*harmonytask.TaskID, error) { + if s.slots.Available() == 0 { + return nil, nil + } + + // check if we have enough huge pages available + // sysctl vm.nr_hugepages should be >= 36 for 32G sectors + if err := hugepageutil.CheckHugePages(36); err != nil { + log.Warnw("huge pages check failed, try 'sudo sysctl -w vm.nr_hugepages=36' and make sure your system uses 1G huge pages", "err", err) + return nil, nil + } + + id := ids[0] + return &id, nil +} + +var ssizeToName = map[abi.SectorSize]string{ + abi.SectorSize(2 << 10): "2K", + abi.SectorSize(8 << 20): "8M", + abi.SectorSize(512 << 20): "512M", + abi.SectorSize(32 << 30): "32G", + abi.SectorSize(64 << 30): "64G", +} + +func (s *SupraSeal) TypeDetails() harmonytask.TaskTypeDetails { + return harmonytask.TaskTypeDetails{ + Max: s.pipelines, + Name: fmt.Sprintf("Batch%d-%s", s.sectors, ssizeToName[must.One(s.spt.SectorSize())]), + Cost: resources.Resources{ + Cpu: 1, + Gpu: 0, + Ram: 16 << 30, + }, + MaxFailures: 4, + IAmBored: passcall.Every(30*time.Second, s.schedule), + } +} + +func (s *SupraSeal) Adder(taskFunc harmonytask.AddTaskFunc) { +} + +func (s *SupraSeal) schedule(taskFunc harmonytask.AddTaskFunc) error { + taskFunc(func(id harmonytask.TaskID, tx *harmonydb.Tx) (shouldCommit bool, seriousError error) { + // claim [sectors] pipeline entries + var sectors []struct { + SpID int64 `db:"sp_id"` + SectorNumber int64 `db:"sector_number"` + TaskIDSDR *int64 `db:"task_id_sdr"` + } + + err := tx.Select(§ors, `SELECT sp_id, sector_number, task_id_sdr FROM sectors_sdr_pipeline + LEFT JOIN harmony_task ht on sectors_sdr_pipeline.task_id_sdr = ht.id + WHERE after_sdr = FALSE AND (task_id_sdr IS NULL OR (ht.owner_id IS NULL AND ht.name = 'SDR')) LIMIT $1`, s.sectors) + if err != nil { + return false, xerrors.Errorf("getting tasks: %w", err) + } + + log.Infow("got sectors, maybe schedule", "sectors", len(sectors), "s.sectors", s.sectors) + + if len(sectors) != s.sectors { + // not enough sectors to fill a batch + log.Infow("not enough sectors to fill a batch", "sectors", len(sectors)) + return false, nil + } + + // assign to pipeline entries, set task_id_sdr, task_id_tree_r, task_id_tree_c + for _, t := range sectors { + _, err := tx.Exec(`UPDATE sectors_sdr_pipeline SET task_id_sdr = $1, task_id_tree_r = $1, task_id_tree_c = $1, task_id_tree_d = $1 WHERE sp_id = $2 AND sector_number = $3`, id, t.SpID, t.SectorNumber) + if err != nil { + return false, xerrors.Errorf("updating task id: %w", err) + } + + if t.TaskIDSDR != nil { + // sdr task exists, remove it from the task engine + _, err := tx.Exec(`DELETE FROM harmony_task WHERE id = $1`, *t.TaskIDSDR) + if err != nil { + return false, xerrors.Errorf("deleting old task: %w", err) + } + } + } + + return true, nil + }) + + return nil +} + +var FSOverheadSupra = map[storiface.SectorFileType]int{ // 10x overheads + storiface.FTUnsealed: storiface.FSOverheadDen, + storiface.FTSealed: storiface.FSOverheadDen, + storiface.FTCache: 11, // C + R' (no 11 layers + D(2x ssize)); +} + +func SupraSpaceUse(ft storiface.SectorFileType, ssize abi.SectorSize) (uint64, error) { + var need uint64 + for _, pathType := range ft.AllSet() { + + oh, ok := FSOverheadSupra[pathType] + if !ok { + return 0, xerrors.Errorf("no seal overhead info for %s", pathType) + } + + need += uint64(oh) * uint64(ssize) / storiface.FSOverheadDen + } + + return need, nil +} + +func init() { + spts := []abi.RegisteredSealProof{ + abi.RegisteredSealProof_StackedDrg32GiBV1_1, + abi.RegisteredSealProof_StackedDrg64GiBV1_1, + } + + batchSizes := []int{1, 2, 4, 8, 16, 32, 64, 128} + + for _, spt := range spts { + for _, batchSize := range batchSizes { + _ = harmonytask.Reg(&SupraSeal{ + spt: spt, + sectors: batchSize, + }) + } + } + +} + +var _ harmonytask.TaskInterface = &SupraSeal{} diff --git a/tasks/winning/winning_task.go b/tasks/winning/winning_task.go index 8bd3d6a5d..d738a9e67 100644 --- a/tasks/winning/winning_task.go +++ b/tasks/winning/winning_task.go @@ -30,6 +30,7 @@ import ( "github.com/filecoin-project/curio/lib/promise" "github.com/filecoin-project/lotus/api" + "github.com/filecoin-project/lotus/chain/actors/policy" "github.com/filecoin-project/lotus/chain/gen" lrand "github.com/filecoin-project/lotus/chain/rand" "github.com/filecoin-project/lotus/chain/types" @@ -185,6 +186,20 @@ func (t *WinPostTask) Do(taskID harmonytask.TaskID, stillOwned func() bool) (don mbi, err := t.api.MinerGetBaseInfo(ctx, maddr, round, base.TipSet.Key()) if err != nil { + // possible that the tipset was really obsolete + log.Errorw("WinPoSt failed to get mining base info", "error", err, "tipset", types.LogCids(base.TipSet.Cids())) + + curHead, err := t.api.ChainHead(ctx) + if err != nil { + return false, xerrors.Errorf("failed to get chain head with miner base info check error: %w", err) + } + + maxAge := policy.ChainFinality + if curHead.Height() > base.TipSet.Height()+maxAge { + log.Warnw("Mining base too old, dropping", "tipset", types.LogCids(base.TipSet.Cids()), "miner", maddr, "curHead", curHead.Height(), "baseHead", base.TipSet.Height(), "diffEpochs", curHead.Height()-base.TipSet.Height()) + return persistNoWin() + } + return false, xerrors.Errorf("failed to get mining base info: %w", err) } if mbi == nil { @@ -488,24 +503,15 @@ func (t *WinPostTask) CanAccept(ids []harmonytask.TaskID, engine *harmonytask.Ta return nil, nil } - // select lowest epoch - var lowestEpoch abi.ChainEpoch - var lowestEpochID = ids[0] + // select task id, hoping to get the highest epoch + var highestTaskID harmonytask.TaskID for _, id := range ids { - var epoch uint64 - err := t.db.QueryRow(context.Background(), `SELECT epoch FROM mining_tasks WHERE task_id = $1`, id).Scan(&epoch) - if err != nil { - log.Errorw("failed to get epoch for task", "task", id, "error", err) - continue - } - - if lowestEpoch == 0 || abi.ChainEpoch(epoch) < lowestEpoch { - lowestEpoch = abi.ChainEpoch(epoch) - lowestEpochID = id + if id > highestTaskID { + highestTaskID = id } } - return &lowestEpochID, nil + return &highestTaskID, nil } func (t *WinPostTask) TypeDetails() harmonytask.TaskTypeDetails { diff --git a/web/static/pages/node_info/node-info.mjs b/web/static/pages/node_info/node-info.mjs index ca6034e50..a5ce32b08 100644 --- a/web/static/pages/node_info/node-info.mjs +++ b/web/static/pages/node_info/node-info.mjs @@ -38,7 +38,10 @@ customElements.define('node-info',class NodeInfoElement extends LitElement { ${this.data.Info.CPU} ${this.toHumanBytes(this.data.Info.Memory)} ${this.data.Info.GPU} - [pprof] + + [pprof] + [metrics] +
diff --git a/web/static/pages/sector/sector-info.mjs b/web/static/pages/sector/sector-info.mjs index 11db50c62..5332ceac1 100644 --- a/web/static/pages/sector/sector-info.mjs +++ b/web/static/pages/sector/sector-info.mjs @@ -14,7 +14,7 @@ customElements.define('sector-info',class SectorInfo extends LitElement { } async removeSector() { await RPCCall('SectorRemove', [this.data.SpID, this.data.SectorNumber]); - window.location.href = '/pages/pipeline_porep/pipeline_porep_sectors'; + window.location.href = '/pages/pipeline_porep/'; } async resumeSector() { await RPCCall('SectorResume', [this.data.SpID, this.data.SectorNumber]);