From 7710a57fa3c263bb9c9e336edcd8bb40c2fb372b Mon Sep 17 00:00:00 2001 From: Aayush Date: Wed, 5 Apr 2023 15:25:57 -0400 Subject: [PATCH 01/46] fix: include extra messages in ComputeState InvocResult output --- chain/stmgr/utils.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/chain/stmgr/utils.go b/chain/stmgr/utils.go index c93267d50..5e3bbd278 100644 --- a/chain/stmgr/utils.go +++ b/chain/stmgr/utils.go @@ -72,7 +72,7 @@ func ComputeState(ctx context.Context, sm *StateManager, height abi.ChainEpoch, base, trace, err := sm.ExecutionTrace(ctx, ts) if err != nil { - return cid.Undef, nil, err + return cid.Undef, nil, xerrors.Errorf("failed to compute base state: %w", err) } for i := ts.Height(); i < height; i++ { @@ -116,6 +116,21 @@ func ComputeState(ctx context.Context, sm *StateManager, height abi.ChainEpoch, if ret.ExitCode != 0 { log.Infof("compute state apply message %d failed (exit: %d): %s", i, ret.ExitCode, ret.ActorErr) } + + ir := &api.InvocResult{ + MsgCid: msg.Cid(), + Msg: msg, + MsgRct: &ret.MessageReceipt, + ExecutionTrace: ret.ExecutionTrace, + Duration: ret.Duration, + } + if ret.ActorErr != nil { + ir.Error = ret.ActorErr.Error() + } + if ret.GasCosts != nil { + ir.GasCost = MakeMsgGasCost(msg, ret) + } + trace = append(trace, ir) } root, err := vmi.Flush(ctx) From ae84f335ccfa1e780ec1b4e5a192bb526f74ffd0 Mon Sep 17 00:00:00 2001 From: Aayush Date: Tue, 11 Apr 2023 10:26:43 -0400 Subject: [PATCH 02/46] feat: pubsub: treat ErrGasFeeCapTooLow as ignore, not reject --- chain/sub/incoming.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/chain/sub/incoming.go b/chain/sub/incoming.go index 87598bd2e..5f15c4ad5 100644 --- a/chain/sub/incoming.go +++ b/chain/sub/incoming.go @@ -377,6 +377,8 @@ func (mv *MessageValidator) Validate(ctx context.Context, pid peer.ID, msg *pubs fallthrough case xerrors.Is(err, messagepool.ErrNonceGap): fallthrough + case xerrors.Is(err, messagepool.ErrGasFeeCapTooLow): + fallthrough case xerrors.Is(err, messagepool.ErrNonceTooLow): return pubsub.ValidationIgnore default: From 8fcf59facca1b79e435722866074a41e44a2444c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Sat, 22 Apr 2023 12:47:11 +0200 Subject: [PATCH 03/46] itests: Test PoSt V1_1 on workers --- itests/worker_test.go | 230 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) diff --git a/itests/worker_test.go b/itests/worker_test.go index 4e845afe8..87bb0047d 100644 --- a/itests/worker_test.go +++ b/itests/worker_test.go @@ -1,6 +1,7 @@ package itests import ( + "bytes" "context" "strings" "sync/atomic" @@ -13,13 +14,17 @@ import ( "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-state-types/abi" + miner11 "github.com/filecoin-project/go-state-types/builtin/v11/miner" + "github.com/filecoin-project/go-state-types/network" "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/build" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/itests/kit" "github.com/filecoin-project/lotus/node" + "github.com/filecoin-project/lotus/node/config" "github.com/filecoin-project/lotus/node/impl" + "github.com/filecoin-project/lotus/node/modules" "github.com/filecoin-project/lotus/node/repo" "github.com/filecoin-project/lotus/storage/paths" sealing "github.com/filecoin-project/lotus/storage/pipeline" @@ -500,3 +505,228 @@ func TestWorkerName(t *testing.T) { require.True(t, found) } + +// Tests that V1_1 proofs on post workers with faults +func TestWindowPostV1P1NV20WorkerFault(t *testing.T) { + kit.QuietMiningLogs() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + blocktime := 2 * time.Millisecond + + sectors := 2 * 48 * 2 + var badsector uint64 = 100000 + + client, miner, _, ens := kit.EnsembleWorker(t, + kit.PresealSectors(sectors), // 2 sectors per partition, 2 partitions in all 48 deadlines + kit.GenesisNetworkVersion(network.Version20), + kit.ConstructorOpts( + node.Override(new(config.ProvingConfig), func() config.ProvingConfig { + c := config.DefaultStorageMiner() + c.Proving.DisableBuiltinWindowPoSt = true + return c.Proving + }), + node.Override(new(*wdpost.WindowPoStScheduler), modules.WindowPostScheduler( + config.DefaultStorageMiner().Fees, + config.ProvingConfig{ + DisableBuiltinWindowPoSt: true, + DisableBuiltinWinningPoSt: false, + DisableWDPoStPreChecks: false, + }, + )), + node.Override(new(paths.Store), func(store *paths.Remote) paths.Store { + return &badWorkerStorage{ + Store: store, + badsector: &badsector, + notBadCount: 1, + } + })), + kit.ThroughRPC(), + kit.WithTaskTypes([]sealtasks.TaskType{sealtasks.TTGenerateWindowPoSt}), + kit.WithWorkerStorage(func(store paths.Store) paths.Store { + return &badWorkerStorage{ + Store: store, + badsector: &badsector, + } + })) + + bm := ens.InterconnectAll().BeginMining(blocktime)[0] + + maddr, err := miner.ActorAddress(ctx) + require.NoError(t, err) + + // wait for some crons(?) + require.Eventually(t, func() bool { + di, err := client.StateMinerProvingDeadline(ctx, maddr, types.EmptyTSK) + require.NoError(t, err) + + parts, err := client.StateMinerPartitions(ctx, maddr, di.Index, types.EmptyTSK) + require.NoError(t, err) + + return len(parts) > 1 + }, 30*time.Second, 100*time.Millisecond) + + // Wait until just before a deadline opens + { + di, err := client.StateMinerProvingDeadline(ctx, maddr, types.EmptyTSK) + require.NoError(t, err) + + di = di.NextNotElapsed() + + t.Log("Running one proving period") + waitUntil := di.Open + di.WPoStChallengeWindow - di.WPoStChallengeLookback - 1 + client.WaitTillChain(ctx, kit.HeightAtLeast(waitUntil)) + + t.Log("Waiting for post message") + bm.Stop() + } + + // Remove one sector in the next deadline (so it's skipped) + { + di, err := client.StateMinerProvingDeadline(ctx, maddr, types.EmptyTSK) + require.NoError(t, err) + + parts, err := client.StateMinerPartitions(ctx, maddr, di.Index+1, types.EmptyTSK) + require.NoError(t, err) + require.Greater(t, len(parts), 0) + + secs := parts[0].AllSectors + n, err := secs.Count() + require.NoError(t, err) + require.Equal(t, uint64(2), n) + + // Drop the sector in second partition + sid, err := secs.First() + require.NoError(t, err) + + t.Logf("Drop sector %d; dl %d part %d", sid, di.Index+1, 0) + + atomic.StoreUint64(&badsector, sid) + require.NoError(t, err) + } + + bm.MineBlocksMustPost(ctx, 2*time.Millisecond) + + mi, err := client.StateMinerInfo(ctx, maddr, types.EmptyTSK) + require.NoError(t, err) + + wact, err := client.StateGetActor(ctx, mi.Worker, types.EmptyTSK) + require.NoError(t, err) + en := wact.Nonce + + // wait for a new message to be sent from worker address, it will be a PoSt + +waitForProof: + for { + //stm: @CHAIN_STATE_GET_ACTOR_001 + wact, err := client.StateGetActor(ctx, mi.Worker, types.EmptyTSK) + require.NoError(t, err) + if wact.Nonce > en { + break waitForProof + } + + build.Clock.Sleep(blocktime) + } + + slm, err := client.StateListMessages(ctx, &api.MessageMatch{To: maddr}, types.EmptyTSK, 0) + require.NoError(t, err) + + pmr, err := client.StateSearchMsg(ctx, types.EmptyTSK, slm[0], -1, false) + require.NoError(t, err) + + nv, err := client.StateNetworkVersion(ctx, pmr.TipSet) + require.NoError(t, err) + require.Equal(t, network.Version20, nv) + + require.True(t, pmr.Receipt.ExitCode.IsSuccess()) + + slmsg, err := client.ChainGetMessage(ctx, slm[0]) + require.NoError(t, err) + + var params miner11.SubmitWindowedPoStParams + require.NoError(t, params.UnmarshalCBOR(bytes.NewBuffer(slmsg.Params))) + require.Equal(t, abi.RegisteredPoStProof_StackedDrgWindow2KiBV1_1, params.Proofs[0].PoStProof) + + require.Len(t, params.Partitions, 2) + sc0, err := params.Partitions[0].Skipped.Count() + require.NoError(t, err) + require.Equal(t, uint64(1), sc0) + sc1, err := params.Partitions[1].Skipped.Count() + require.NoError(t, err) + require.Equal(t, uint64(0), sc1) +} + +// Tests that V1_1 proofs on post worker +func TestWindowPostV1P1NV20Worker(t *testing.T) { + kit.QuietMiningLogs() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + blocktime := 2 * time.Millisecond + + client, miner, _, ens := kit.EnsembleWorker(t, + kit.GenesisNetworkVersion(network.Version20), + kit.ConstructorOpts( + node.Override(new(config.ProvingConfig), func() config.ProvingConfig { + c := config.DefaultStorageMiner() + c.Proving.DisableBuiltinWindowPoSt = true + return c.Proving + }), + node.Override(new(*wdpost.WindowPoStScheduler), modules.WindowPostScheduler( + config.DefaultStorageMiner().Fees, + config.ProvingConfig{ + DisableBuiltinWindowPoSt: true, + DisableBuiltinWinningPoSt: false, + DisableWDPoStPreChecks: false, + }, + ))), + kit.ThroughRPC(), + kit.WithTaskTypes([]sealtasks.TaskType{sealtasks.TTGenerateWindowPoSt})) + + ens.InterconnectAll().BeginMining(blocktime) + + maddr, err := miner.ActorAddress(ctx) + require.NoError(t, err) + + mi, err := client.StateMinerInfo(ctx, maddr, types.EmptyTSK) + require.NoError(t, err) + + wact, err := client.StateGetActor(ctx, mi.Worker, types.EmptyTSK) + require.NoError(t, err) + en := wact.Nonce + + // wait for a new message to be sent from worker address, it will be a PoSt + +waitForProof: + for { + //stm: @CHAIN_STATE_GET_ACTOR_001 + wact, err := client.StateGetActor(ctx, mi.Worker, types.EmptyTSK) + require.NoError(t, err) + if wact.Nonce > en { + break waitForProof + } + + build.Clock.Sleep(blocktime) + } + + slm, err := client.StateListMessages(ctx, &api.MessageMatch{To: maddr}, types.EmptyTSK, 0) + require.NoError(t, err) + + pmr, err := client.StateSearchMsg(ctx, types.EmptyTSK, slm[0], -1, false) + require.NoError(t, err) + + nv, err := client.StateNetworkVersion(ctx, pmr.TipSet) + require.NoError(t, err) + require.Equal(t, network.Version20, nv) + + require.True(t, pmr.Receipt.ExitCode.IsSuccess()) + + slmsg, err := client.ChainGetMessage(ctx, slm[0]) + require.NoError(t, err) + + var params miner11.SubmitWindowedPoStParams + require.NoError(t, params.UnmarshalCBOR(bytes.NewBuffer(slmsg.Params))) + require.Equal(t, abi.RegisteredPoStProof_StackedDrgWindow2KiBV1_1, params.Proofs[0].PoStProof) +} From 70d2899ead8815bb5366c9cf8bbe5bdcfcbf6bc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Tue, 9 May 2023 19:11:15 +0200 Subject: [PATCH 04/46] itests: wdpost: Address review --- itests/worker_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/itests/worker_test.go b/itests/worker_test.go index 87bb0047d..246c842c5 100644 --- a/itests/worker_test.go +++ b/itests/worker_test.go @@ -556,7 +556,7 @@ func TestWindowPostV1P1NV20WorkerFault(t *testing.T) { maddr, err := miner.ActorAddress(ctx) require.NoError(t, err) - // wait for some crons(?) + // wait for sectors to be committed require.Eventually(t, func() bool { di, err := client.StateMinerProvingDeadline(ctx, maddr, types.EmptyTSK) require.NoError(t, err) @@ -596,11 +596,11 @@ func TestWindowPostV1P1NV20WorkerFault(t *testing.T) { require.NoError(t, err) require.Equal(t, uint64(2), n) - // Drop the sector in second partition + // Drop the sector in first partition sid, err := secs.First() require.NoError(t, err) - t.Logf("Drop sector %d; dl %d part %d", sid, di.Index+1, 0) + t.Logf("Drop sector %d; dl %d part %d", sid, di.Index, 0) atomic.StoreUint64(&badsector, sid) require.NoError(t, err) From 4ca30abeef39714d9d684d03dae406bbf238448b Mon Sep 17 00:00:00 2001 From: Fridrik Asmundsson Date: Fri, 28 Apr 2023 10:16:55 +0000 Subject: [PATCH 05/46] Add support for blockHash param in eth_getLogs --- node/impl/full/eth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index a26241cd7..2f1b30fad 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -1209,7 +1209,7 @@ func (e *EthEvent) installEthFilterSpec(ctx context.Context, filterSpec *ethtype return nil, xerrors.Errorf("must not specify block hash and from/to block") } - // TODO: derive a tipset hash from eth hash - might need to push this down into the EventFilterManager + tipsetCid = filterSpec.BlockHash.ToCid() } else { if filterSpec.FromBlock == nil || *filterSpec.FromBlock == "latest" { ts := e.Chain.GetHeaviestTipSet() From dbb892d89f9abc83a438a87487c5ff13d1a3ea50 Mon Sep 17 00:00:00 2001 From: Maciej Witowski Date: Fri, 28 Apr 2023 17:01:11 +0200 Subject: [PATCH 06/46] lotus-fountain: make compatible with 0x addresses #10560 --- cmd/lotus-fountain/main.go | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/cmd/lotus-fountain/main.go b/cmd/lotus-fountain/main.go index 780aef916..464251cd3 100644 --- a/cmd/lotus-fountain/main.go +++ b/cmd/lotus-fountain/main.go @@ -7,6 +7,7 @@ import ( "net" "net/http" "os" + "strings" "time" rice "github.com/GeertJohan/go.rice" @@ -19,6 +20,7 @@ import ( "github.com/filecoin-project/lotus/api/v0api" "github.com/filecoin-project/lotus/build" "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/lotus/chain/types/ethtypes" lcli "github.com/filecoin-project/lotus/cli" ) @@ -193,14 +195,34 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - to, err := address.NewFromString(r.FormValue("address")) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - if to == address.Undef { - http.Error(w, "empty address", http.StatusBadRequest) - return + var to address.Address + + addressInput := r.FormValue("address") + if strings.HasPrefix(addressInput, "0x") { + ethAddress, err := ethtypes.ParseEthAddress(addressInput) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + filecoinAddress, err := ethAddress.ToFilecoinAddress() + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + to = filecoinAddress + } else { + filecoinAddress, err := address.NewFromString(addressInput) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if filecoinAddress == address.Undef { + http.Error(w, "empty address", http.StatusBadRequest) + return + } + + to = filecoinAddress } // Limit based on wallet address From 08e6e041454785320d4372ac94fddd680eca293e Mon Sep 17 00:00:00 2001 From: Maciej Witowski Date: Fri, 5 May 2023 13:16:48 +0200 Subject: [PATCH 07/46] Unify error handling --- cmd/lotus-fountain/main.go | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/cmd/lotus-fountain/main.go b/cmd/lotus-fountain/main.go index 464251cd3..68f3d0e99 100644 --- a/cmd/lotus-fountain/main.go +++ b/cmd/lotus-fountain/main.go @@ -195,9 +195,11 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - var to address.Address - addressInput := r.FormValue("address") + + var filecoinAddress address.Address + var decodeError error + if strings.HasPrefix(addressInput, "0x") { ethAddress, err := ethtypes.ParseEthAddress(addressInput) if err != nil { @@ -205,28 +207,22 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - filecoinAddress, err := ethAddress.ToFilecoinAddress() - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - to = filecoinAddress + filecoinAddress, decodeError = ethAddress.ToFilecoinAddress() } else { - filecoinAddress, err := address.NewFromString(addressInput) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - if filecoinAddress == address.Undef { - http.Error(w, "empty address", http.StatusBadRequest) - return - } + filecoinAddress, decodeError = address.NewFromString(addressInput) + } - to = filecoinAddress + if decodeError != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if filecoinAddress == address.Undef { + http.Error(w, "empty address", http.StatusBadRequest) + return } // Limit based on wallet address - limiter := h.limiter.GetWalletLimiter(to.String()) + limiter := h.limiter.GetWalletLimiter(filecoinAddress.String()) if !limiter.Allow() { http.Error(w, http.StatusText(http.StatusTooManyRequests)+": wallet limit", http.StatusTooManyRequests) return @@ -252,7 +248,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { smsg, err := h.api.MpoolPushMessage(h.ctx, &types.Message{ Value: types.BigInt(h.sendPerRequest), From: h.from, - To: to, + To: filecoinAddress, }, nil) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) From 0ec3d442766e8476d85a65bf5381c80824dc9cb0 Mon Sep 17 00:00:00 2001 From: Jianhui Xie Date: Wed, 10 May 2023 23:30:43 -0700 Subject: [PATCH 08/46] add grant-datacap support for lotus fountain --- cmd/lotus-fountain/main.go | 60 +++++++++++++++++++++++++--- cmd/lotus-fountain/site/datacap.html | 40 +++++++++++++++++++ cmd/lotus-fountain/site/index.html | 6 +++ 3 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 cmd/lotus-fountain/site/datacap.html diff --git a/cmd/lotus-fountain/main.go b/cmd/lotus-fountain/main.go index 780aef916..d0e1964c4 100644 --- a/cmd/lotus-fountain/main.go +++ b/cmd/lotus-fountain/main.go @@ -3,6 +3,8 @@ package main import ( "context" "fmt" + verifregtypes9 "github.com/filecoin-project/go-state-types/builtin/v9/verifreg" + "github.com/filecoin-project/lotus/chain/actors" "html/template" "net" "net/http" @@ -18,6 +20,7 @@ import ( "github.com/filecoin-project/lotus/api/v0api" "github.com/filecoin-project/lotus/build" + "github.com/filecoin-project/lotus/chain/actors/builtin/verifreg" "github.com/filecoin-project/lotus/chain/types" lcli "github.com/filecoin-project/lotus/cli" ) @@ -70,6 +73,11 @@ var runCmd = &cli.Command{ EnvVars: []string{"LOTUS_FOUNTAIN_AMOUNT"}, Value: "50", }, + &cli.Uint64Flag{ + Name: "data-cap", + EnvVars: []string{"LOTUS_DATACAP_AMOUNT"}, + Value: 10240, + }, &cli.Float64Flag{ Name: "captcha-threshold", Value: 0.5, @@ -108,6 +116,7 @@ var runCmd = &cli.Command{ ctx: ctx, api: nodeApi, from: from, + allowance: types.NewInt(cctx.Uint64("data-cap")), sendPerRequest: sendPerRequest, limiter: NewLimiter(LimiterConfig{ TotalRate: 500 * time.Millisecond, @@ -124,6 +133,8 @@ var runCmd = &cli.Command{ http.Handle("/", http.FileServer(box.HTTPBox())) http.HandleFunc("/funds.html", prepFundsHtml(box)) http.Handle("/send", h) + http.HandleFunc("/datacap.html", prepDataCapHtml(box)) + http.Handle("/datacap", h) fmt.Printf("Open http://%s\n", cctx.String("front")) go func() { @@ -156,12 +167,24 @@ func prepFundsHtml(box *rice.Box) http.HandlerFunc { } } +func prepDataCapHtml(box *rice.Box) http.HandlerFunc { + tmpl := template.Must(template.New("datacaps").Parse(box.MustString("datacap.html"))) + return func(w http.ResponseWriter, r *http.Request) { + err := tmpl.Execute(w, os.Getenv("RECAPTCHA_SITE_KEY")) + if err != nil { + http.Error(w, err.Error(), http.StatusBadGateway) + return + } + } +} + type handler struct { ctx context.Context api v0api.FullNode from address.Address sendPerRequest types.FIL + allowance types.BigInt limiter *Limiter recapThreshold float64 @@ -187,6 +210,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusBadGateway) return } + if !capResp.Success || capResp.Score < h.recapThreshold { log.Infow("spam", "capResp", capResp) http.Error(w, "spam protection", http.StatusUnprocessableEntity) @@ -227,11 +251,37 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - smsg, err := h.api.MpoolPushMessage(h.ctx, &types.Message{ - Value: types.BigInt(h.sendPerRequest), - From: h.from, - To: to, - }, nil) + var smsg *types.SignedMessage + if r.RequestURI == "/send" { + smsg, err = h.api.MpoolPushMessage( + h.ctx, &types.Message{ + Value: types.BigInt(h.sendPerRequest), + From: h.from, + To: to, + }, nil) + } else if r.RequestURI == "/datacap" { + var params []byte + params, err = actors.SerializeParams( + &verifregtypes9.AddVerifiedClientParams{ + Address: to, + Allowance: h.allowance, + }) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + smsg, err = h.api.MpoolPushMessage( + h.ctx, &types.Message{ + Params: params, + From: h.from, + To: verifreg.Address, + Method: verifreg.Methods.AddVerifiedClient, + }, nil) + } else { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return diff --git a/cmd/lotus-fountain/site/datacap.html b/cmd/lotus-fountain/site/datacap.html new file mode 100644 index 000000000..10cf1f411 --- /dev/null +++ b/cmd/lotus-fountain/site/datacap.html @@ -0,0 +1,40 @@ + + + + Grant DataCap - Lotus Fountain + + + + + + +
+
+
+ [GRANT DATACAP] +
+
+
+ Enter destination address: + + +
+
+
+ + +
+ + diff --git a/cmd/lotus-fountain/site/index.html b/cmd/lotus-fountain/site/index.html index 644960225..4dc3df478 100644 --- a/cmd/lotus-fountain/site/index.html +++ b/cmd/lotus-fountain/site/index.html @@ -13,6 +13,12 @@ +
+ [LOTUS DEVNET GRANT DATACAP] +
+