Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(f3): test API during inactive-F3 modes and make consistent and safe #12781

Merged
merged 5 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions chain/lf3/f3.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,20 +184,23 @@ func (fff *F3) GetLatestCert(ctx context.Context) (*certs.FinalityCertificate, e
return fff.inner.GetLatestCert(ctx)
}

func (fff *F3) GetManifest(ctx context.Context) *manifest.Manifest {
func (fff *F3) GetManifest(ctx context.Context) (*manifest.Manifest, error) {
m := fff.inner.Manifest()
if m == nil {
return nil, xerrors.New("no known network manifest")
}
if m.InitialPowerTable.Defined() {
return m
return m, nil
}
cert0, err := fff.inner.GetCert(ctx, 0)
if err != nil {
return m
return m, nil // return manifest without power table
}

var mCopy = *m
m = &mCopy
m.InitialPowerTable = cert0.ECChain.Base().PowerTable
return m
return m, nil
}

func (fff *F3) GetPowerTable(ctx context.Context, tsk types.TipSetKey) (gpbft.PowerEntries, error) {
Expand Down
130 changes: 129 additions & 1 deletion itests/f3_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package itests

import (
"context"
"reflect"
"sync"
"testing"
"time"
Expand All @@ -14,10 +15,12 @@ import (
"golang.org/x/sync/errgroup"

"github.com/filecoin-project/go-address"
"github.com/filecoin-project/go-f3/certs"
"github.com/filecoin-project/go-f3/gpbft"
"github.com/filecoin-project/go-f3/manifest"
"github.com/filecoin-project/go-state-types/abi"

"github.com/filecoin-project/lotus/api"
lotus_api "github.com/filecoin-project/lotus/api"
"github.com/filecoin-project/lotus/chain/lf3"
"github.com/filecoin-project/lotus/chain/types"
Expand Down Expand Up @@ -57,6 +60,129 @@ func TestF3_Enabled(t *testing.T) {
e.requireAllMinersParticipate()
}

// TestF3_Disabled tests the return values and errors of the F3 API when F3 is
// disabled or is not yet running.
func TestF3_InactiveModes(t *testing.T) {
kit.QuietMiningLogs()

testCases := []struct {
mode string
expectedErrors map[string]any
expectedValues map[string]any
customValidateReturn map[string]func(t *testing.T, ret []reflect.Value)
}{
{
mode: "disabled",
expectedErrors: map[string]any{
"F3GetOrRenewParticipationTicket": lotus_api.ErrF3Disabled,
"F3Participate": lotus_api.ErrF3Disabled,
"F3GetCertificate": lotus_api.ErrF3Disabled,
"F3GetLatestCertificate": lotus_api.ErrF3Disabled,
"F3GetManifest": lotus_api.ErrF3Disabled,
"F3GetECPowerTable": lotus_api.ErrF3Disabled,
"F3GetF3PowerTable": lotus_api.ErrF3Disabled,
"F3IsRunning": lotus_api.ErrF3Disabled,
},
expectedValues: map[string]any{
"F3GetOrRenewParticipationTicket": (api.F3ParticipationTicket)(nil),
"F3Participate": api.F3ParticipationLease{},
"F3GetCertificate": (*certs.FinalityCertificate)(nil),
"F3GetLatestCertificate": (*certs.FinalityCertificate)(nil),
"F3GetManifest": (*manifest.Manifest)(nil),
"F3GetECPowerTable": (gpbft.PowerEntries)(nil),
"F3GetF3PowerTable": (gpbft.PowerEntries)(nil),
"F3IsRunning": false,
},
},
{
mode: "not running",
expectedErrors: map[string]any{
"F3GetOrRenewParticipationTicket": api.ErrF3NotReady,
"F3Participate": api.ErrF3NotReady,
"F3GetCertificate": "F3 is not running",
"F3GetLatestCertificate": "F3 is not running",
"F3GetManifest": "no known network manifest",
"F3GetF3PowerTable": "no known network manifest",
},
expectedValues: map[string]any{
"F3GetOrRenewParticipationTicket": (api.F3ParticipationTicket)(nil),
"F3Participate": api.F3ParticipationLease{},
"F3GetCertificate": (*certs.FinalityCertificate)(nil),
"F3GetLatestCertificate": (*certs.FinalityCertificate)(nil),
"F3GetManifest": (*manifest.Manifest)(nil),
"F3GetF3PowerTable": (gpbft.PowerEntries)(nil),
"F3IsRunning": false,
},
customValidateReturn: map[string]func(t *testing.T, ret []reflect.Value){
"F3GetECPowerTable": func(t *testing.T, ret []reflect.Value) {
// special case because it simply returns power table from EC which is not F3 dependent
require.NotNil(t, ret[0].Interface(), "unexpected return value")
},
},
},
}

for _, tc := range testCases {
t.Run(tc.mode, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()

opts := []any{kit.MockProofs()}
if tc.mode == "disabled" {
opts = append(opts, kit.F3Enabled(nil))
}

client, miner, ens := kit.EnsembleMinimal(t, opts...)
ens.InterconnectAll().BeginMining(2 * time.Millisecond)
ens.Start()

head := client.WaitTillChain(ctx, kit.HeightAtLeast(10))

rctx := reflect.ValueOf(ctx)
rtsk := reflect.ValueOf(head.Key())
rminer := reflect.ValueOf(miner.ActorAddr)
rticket := reflect.ValueOf([]byte("fish"))
rone := reflect.ValueOf(uint64(1))

calls := map[string][]reflect.Value{
"F3GetOrRenewParticipationTicket": {rctx, rminer, rticket, rone},
"F3Participate": {rctx, rticket},
"F3GetCertificate": {rctx, rone},
"F3GetLatestCertificate": {rctx},
"F3GetManifest": {rctx},
"F3GetECPowerTable": {rctx, rtsk},
"F3GetF3PowerTable": {rctx, rtsk},
"F3IsRunning": {rctx},
}

for fn, args := range calls {
t.Run(fn, func(t *testing.T) {
ret := reflect.ValueOf(client).MethodByName(fn).Call(args)

if expectedValue, hasExpectedValue := tc.expectedValues[fn]; hasExpectedValue {
require.Equal(t, expectedValue, ret[0].Interface(), "unexpected return value")
}

if expectedError, hasExpectedError := tc.expectedErrors[fn]; hasExpectedError {
switch err := expectedError.(type) {
case error:
require.ErrorIs(t, ret[1].Interface().(error), err, "unexpected error")
case string:
require.ErrorContains(t, ret[1].Interface().(error), err, "unexpected error")
}
} else {
require.Nil(t, ret[1].Interface(), "unexpected error")
}

if validate, hasValidate := tc.customValidateReturn[fn]; hasValidate {
validate(t, ret)
}
})
}
})
}
}

// TestF3_Rebootstrap tests F3 can be rebootsrapped by changing the manifest
// without disrupting miner participation.
func TestF3_Rebootstrap(t *testing.T) {
Expand Down Expand Up @@ -227,7 +353,9 @@ func (e *testEnv) waitTillF3Instance(i uint64, timeout time.Duration) {
func (e *testEnv) waitTillManifestChange(newManifest *manifest.Manifest, timeout time.Duration) {
e.waitFor(func(n *kit.TestFullNode) bool {
m, err := n.F3GetManifest(e.testCtx)
require.NoError(e.t, err)
if err != nil || m == nil {
return false
}
return newManifest.Equal(m)
}, timeout)
}
Expand Down
6 changes: 3 additions & 3 deletions node/impl/full/f3.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ type F3API struct {
func (f3api *F3API) F3GetOrRenewParticipationTicket(ctx context.Context, miner address.Address, previous api.F3ParticipationTicket, instances uint64) (api.F3ParticipationTicket, error) {
if f3api.F3 == nil {
log.Infof("F3GetParticipationTicket called for %v, F3 is disabled", miner)
return api.F3ParticipationTicket{}, api.ErrF3Disabled
return nil, api.ErrF3Disabled
}
minerID, err := address.IDFromAddress(miner)
if err != nil {
return api.F3ParticipationTicket{}, xerrors.Errorf("miner address is not of ID type: %v: %w", miner, err)
return nil, xerrors.Errorf("miner address is not of ID type: %v: %w", miner, err)
}
return f3api.F3.GetOrRenewParticipationTicket(ctx, minerID, previous, instances)
}
Expand Down Expand Up @@ -61,7 +61,7 @@ func (f3api *F3API) F3GetManifest(ctx context.Context) (*manifest.Manifest, erro
if f3api.F3 == nil {
return nil, api.ErrF3Disabled
}
return f3api.F3.GetManifest(ctx), nil
return f3api.F3.GetManifest(ctx)
}

func (f3api *F3API) F3IsRunning(context.Context) (bool, error) {
Expand Down
Loading