lotus/storage/wdpost/wdpost_changehandler_test.go
2023-11-15 13:06:51 +01:00

1228 lines
39 KiB
Go

// stm: #unit
package wdpost
import (
"context"
"fmt"
"sync"
"testing"
"time"
"github.com/ipfs/go-cid"
"github.com/stretchr/testify/require"
"github.com/filecoin-project/go-address"
"github.com/filecoin-project/go-state-types/abi"
minertypes "github.com/filecoin-project/go-state-types/builtin/v9/miner"
"github.com/filecoin-project/go-state-types/crypto"
"github.com/filecoin-project/go-state-types/dline"
tutils "github.com/filecoin-project/specs-actors/support/testing"
"github.com/filecoin-project/lotus/chain/types"
)
var dummyCid cid.Cid
func init() {
dummyCid, _ = cid.Parse("bafkqaaa")
}
type proveRes struct {
posts []minertypes.SubmitWindowedPoStParams
err error
}
type postStatus string
const (
postStatusStart postStatus = "postStatusStart"
postStatusProving postStatus = "postStatusProving"
postStatusComplete postStatus = "postStatusComplete"
)
type mockAPI struct {
ch *changeHandler
deadline *dline.Info
proveResult chan *proveRes
submitResult chan error
onStateChange chan struct{}
tsLock sync.RWMutex
ts map[types.TipSetKey]*types.TipSet
abortCalledLock sync.RWMutex
abortCalled bool
statesLk sync.RWMutex
postStates map[abi.ChainEpoch]postStatus
}
func newMockAPI() *mockAPI {
return &mockAPI{
proveResult: make(chan *proveRes),
onStateChange: make(chan struct{}),
submitResult: make(chan error),
postStates: make(map[abi.ChainEpoch]postStatus),
ts: make(map[types.TipSetKey]*types.TipSet),
}
}
func (m *mockAPI) makeTs(t *testing.T, h abi.ChainEpoch) *types.TipSet {
m.tsLock.Lock()
defer m.tsLock.Unlock()
ts := makeTs(t, h)
m.ts[ts.Key()] = ts
return ts
}
func (m *mockAPI) setDeadline(di *dline.Info) {
m.tsLock.Lock()
defer m.tsLock.Unlock()
m.deadline = di
}
func (m *mockAPI) getDeadline(currentEpoch abi.ChainEpoch) *dline.Info {
close := minertypes.WPoStChallengeWindow - 1
dlIdx := uint64(0)
for close < currentEpoch {
close += minertypes.WPoStChallengeWindow
dlIdx++
}
return NewDeadlineInfo(0, dlIdx, currentEpoch)
}
func (m *mockAPI) StateMinerProvingDeadline(ctx context.Context, address address.Address, key types.TipSetKey) (*dline.Info, error) {
m.tsLock.RLock()
defer m.tsLock.RUnlock()
ts, ok := m.ts[key]
if !ok {
panic(fmt.Sprintf("unexpected tipset key %s", key))
}
if m.deadline != nil {
m.deadline.CurrentEpoch = ts.Height()
return m.deadline, nil
}
return m.getDeadline(ts.Height()), nil
}
func (m *mockAPI) startGeneratePoST(
ctx context.Context,
ts *types.TipSet,
deadline *dline.Info,
completeGeneratePoST CompleteGeneratePoSTCb,
) context.CancelFunc {
ctx, cancel := context.WithCancel(ctx)
log.Errorf("mock posting\n")
m.statesLk.Lock()
defer m.statesLk.Unlock()
m.postStates[deadline.Open] = postStatusProving
go func() {
defer cancel()
select {
case psRes := <-m.proveResult:
m.statesLk.Lock()
{
if psRes.err == nil {
m.postStates[deadline.Open] = postStatusComplete
} else {
m.postStates[deadline.Open] = postStatusStart
}
}
m.statesLk.Unlock()
completeGeneratePoST(psRes.posts, psRes.err)
case <-ctx.Done():
completeGeneratePoST(nil, ctx.Err())
}
}()
return cancel
}
func (m *mockAPI) getPostStatus(di *dline.Info) postStatus {
m.statesLk.RLock()
defer m.statesLk.RUnlock()
status, ok := m.postStates[di.Open]
if ok {
return status
}
return postStatusStart
}
func (m *mockAPI) startSubmitPoST(
ctx context.Context,
ts *types.TipSet,
deadline *dline.Info,
posts []minertypes.SubmitWindowedPoStParams,
completeSubmitPoST CompleteSubmitPoSTCb,
) context.CancelFunc {
ctx, cancel := context.WithCancel(ctx)
go func() {
defer cancel()
select {
case err := <-m.submitResult:
completeSubmitPoST(err)
case <-ctx.Done():
completeSubmitPoST(ctx.Err())
}
}()
return cancel
}
func (m *mockAPI) onAbort(ts *types.TipSet, deadline *dline.Info) {
m.abortCalledLock.Lock()
defer m.abortCalledLock.Unlock()
m.abortCalled = true
}
func (m *mockAPI) wasAbortCalled() bool {
m.abortCalledLock.RLock()
defer m.abortCalledLock.RUnlock()
return m.abortCalled
}
func (m *mockAPI) recordPoStFailure(err error, ts *types.TipSet, deadline *dline.Info) {
}
func (m *mockAPI) setChangeHandler(ch *changeHandler) {
m.ch = ch
}
// TestChangeHandlerBasic verifies we can generate a proof and submit it
func TestChangeHandlerBasic(t *testing.T) {
//stm: @WDPOST_CHANGE_HANDLER_START_001, @WDPOST_CHANGE_HANDLER_UPDATE_001
//stm: @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_001, @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_PW_001
//stm: @WDPOST_SUBMIT_HANDLER_SUBMIT_IF_READY_004, @WDPOST_PROVE_HANDLER_PROCESS_POST_RESULT_001
//stm: @WDPOST_SUBMIT_HANDLER_PROCESS_PROCESS_RESULTS_001
s := makeScaffolding(t)
mock := s.mock
defer s.ch.shutdown()
s.ch.start()
// Trigger a head change
currentEpoch := abi.ChainEpoch(1)
go triggerHeadAdvance(t, s, currentEpoch)
// Should start proving
<-s.ch.proveHdlr.processedHeadChanges
di := mock.getDeadline(currentEpoch)
require.Equal(t, postStatusProving, s.mock.getPostStatus(di))
// Submitter doesn't have anything to do yet
<-s.ch.submitHdlr.processedHeadChanges
require.Equal(t, SubmitStateStart, s.submitState(di))
// Send a response to the call to generate proofs
posts := []minertypes.SubmitWindowedPoStParams{{Deadline: di.Index}}
mock.proveResult <- &proveRes{posts: posts}
// Should move to proving complete
<-s.ch.proveHdlr.processedPostResults
require.Equal(t, postStatusComplete, s.mock.getPostStatus(di))
// Move to the correct height to submit the proof
currentEpoch = 1 + SubmitConfidence
go triggerHeadAdvance(t, s, currentEpoch)
// Should move to submitting state
<-s.ch.submitHdlr.processedHeadChanges
di = mock.getDeadline(currentEpoch)
require.Equal(t, SubmitStateSubmitting, s.submitState(di))
// Send a response to the submit call
mock.submitResult <- nil
// Should move to the complete state
<-s.ch.submitHdlr.processedSubmitResults
require.Equal(t, SubmitStateComplete, s.submitState(di))
}
// TestChangeHandlerFromProvingToSubmittingNoHeadChange tests that when the
// chain is already advanced past the confidence interval, we should move from
// proving to submitting without a head change in between.
func TestChangeHandlerFromProvingToSubmittingNoHeadChange(t *testing.T) {
//stm: @WDPOST_CHANGE_HANDLER_START_001, @WDPOST_CHANGE_HANDLER_UPDATE_001
//stm: @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_001, @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_PW_001
//stm: @WDPOST_SUBMIT_HANDLER_SUBMIT_IF_READY_004, @WDPOST_SUBMIT_HANDLER_SUBMIT_IF_READY_005
//stm: @WDPOST_PROVE_HANDLER_PROCESS_POST_RESULT_001
s := makeScaffolding(t)
mock := s.mock
// Monitor submit handler's processing of incoming postInfo
s.ch.submitHdlr.processedPostReady = make(chan *postInfo)
defer s.ch.shutdown()
s.ch.start()
// Trigger a head change
currentEpoch := abi.ChainEpoch(1)
go triggerHeadAdvance(t, s, currentEpoch)
// Should start proving
<-s.ch.proveHdlr.processedHeadChanges
di := mock.getDeadline(currentEpoch)
require.Equal(t, postStatusProving, s.mock.getPostStatus(di))
// Submitter doesn't have anything to do yet
<-s.ch.submitHdlr.processedHeadChanges
require.Equal(t, SubmitStateStart, s.submitState(di))
// Trigger a head change that advances the chain beyond the submit
// confidence
currentEpoch = 1 + SubmitConfidence
go triggerHeadAdvance(t, s, currentEpoch)
// Should be no change to state yet
<-s.ch.proveHdlr.processedHeadChanges
require.Equal(t, postStatusProving, s.mock.getPostStatus(di))
<-s.ch.submitHdlr.processedHeadChanges
require.Equal(t, SubmitStateStart, s.submitState(di))
// Send a response to the call to generate proofs
posts := []minertypes.SubmitWindowedPoStParams{{Deadline: di.Index}}
mock.proveResult <- &proveRes{posts: posts}
// Should move to proving complete
<-s.ch.proveHdlr.processedPostResults
di = mock.getDeadline(currentEpoch)
require.Equal(t, postStatusComplete, s.mock.getPostStatus(di))
// Should move directly to submitting state with no further head changes
<-s.ch.submitHdlr.processedPostReady
require.Equal(t, SubmitStateSubmitting, s.submitState(di))
}
// TestChangeHandlerFromProvingEmptyProofsToComplete tests that when there are no
// proofs generated we should not submit anything to chain but submit state
// should move to completed
func TestChangeHandlerFromProvingEmptyProofsToComplete(t *testing.T) {
//stm: @WDPOST_CHANGE_HANDLER_START_001, @WDPOST_CHANGE_HANDLER_UPDATE_001
//stm: @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_001, @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_PW_001
//stm: @WDPOST_SUBMIT_HANDLER_SUBMIT_IF_READY_004, @WDPOST_SUBMIT_HANDLER_SUBMIT_IF_READY_005, @WDPOST_SUBMIT_HANDLER_SUBMIT_IF_READY_006
//stm: @WDPOST_PROVE_HANDLER_PROCESS_POST_RESULT_001
s := makeScaffolding(t)
mock := s.mock
// Monitor submit handler's processing of incoming postInfo
s.ch.submitHdlr.processedPostReady = make(chan *postInfo)
defer s.ch.shutdown()
s.ch.start()
// Trigger a head change
currentEpoch := abi.ChainEpoch(1)
go triggerHeadAdvance(t, s, currentEpoch)
// Should start proving
<-s.ch.proveHdlr.processedHeadChanges
di := mock.getDeadline(currentEpoch)
require.Equal(t, postStatusProving, s.mock.getPostStatus(di))
// Submitter doesn't have anything to do yet
<-s.ch.submitHdlr.processedHeadChanges
require.Equal(t, SubmitStateStart, s.submitState(di))
// Trigger a head change that advances the chain beyond the submit
// confidence
currentEpoch = 1 + SubmitConfidence
go triggerHeadAdvance(t, s, currentEpoch)
// Should be no change to state yet
<-s.ch.proveHdlr.processedHeadChanges
require.Equal(t, postStatusProving, s.mock.getPostStatus(di))
<-s.ch.submitHdlr.processedHeadChanges
require.Equal(t, SubmitStateStart, s.submitState(di))
// Send a response to the call to generate proofs with an empty proofs array
posts := []minertypes.SubmitWindowedPoStParams{}
mock.proveResult <- &proveRes{posts: posts}
// Should move to proving complete
<-s.ch.proveHdlr.processedPostResults
di = mock.getDeadline(currentEpoch)
require.Equal(t, postStatusComplete, s.mock.getPostStatus(di))
// Should move directly to submitting complete state
<-s.ch.submitHdlr.processedPostReady
require.Equal(t, SubmitStateComplete, s.submitState(di))
}
// TestChangeHandlerDontStartUntilProvingPeriod tests that the handler
// ignores updates until the proving period has been reached.
func TestChangeHandlerDontStartUntilProvingPeriod(t *testing.T) {
//stm: @WDPOST_CHANGE_HANDLER_START_001, @WDPOST_CHANGE_HANDLER_UPDATE_001
//stm: @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_001, @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_PW_001
//stm: @WDPOST_SUBMIT_HANDLER_SUBMIT_IF_READY_004
s := makeScaffolding(t)
mock := s.mock
periodStart := minertypes.WPoStProvingPeriod
dlIdx := uint64(1)
currentEpoch := abi.ChainEpoch(10)
di := NewDeadlineInfo(periodStart, dlIdx, currentEpoch)
mock.setDeadline(di)
defer s.ch.shutdown()
s.ch.start()
// Trigger a head change
go triggerHeadAdvance(t, s, currentEpoch)
// Nothing should happen because the proving period has not started
select {
case <-s.ch.proveHdlr.processedHeadChanges:
require.Fail(t, "unexpected prove change")
case <-s.ch.submitHdlr.processedHeadChanges:
require.Fail(t, "unexpected submit change")
case <-time.After(10 * time.Millisecond):
}
// Advance the head to the next proving period's first epoch
currentEpoch = periodStart + minertypes.WPoStChallengeWindow
di = NewDeadlineInfo(periodStart, dlIdx, currentEpoch)
mock.setDeadline(di)
go triggerHeadAdvance(t, s, currentEpoch)
// Should start proving
<-s.ch.proveHdlr.processedHeadChanges
require.Equal(t, postStatusProving, s.mock.getPostStatus(di))
}
// TestChangeHandlerStartProvingNextDeadline verifies that the proof handler
// starts proving the next deadline after the current one
func TestChangeHandlerStartProvingNextDeadline(t *testing.T) {
//stm: @WDPOST_CHANGE_HANDLER_START_001, @WDPOST_CHANGE_HANDLER_UPDATE_001
//stm: @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_001, @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_PW_001
//stm: @WDPOST_SUBMIT_HANDLER_SUBMIT_IF_READY_004, @WDPOST_PROVE_HANDLER_PROCESS_POST_RESULT_001
s := makeScaffolding(t)
mock := s.mock
defer s.ch.shutdown()
s.ch.start()
// Trigger a head change
currentEpoch := abi.ChainEpoch(1)
go triggerHeadAdvance(t, s, currentEpoch+ChallengeConfidence)
// Should start proving
<-s.ch.proveHdlr.processedHeadChanges
di := mock.getDeadline(currentEpoch)
require.Equal(t, postStatusProving, s.mock.getPostStatus(di))
// Trigger a head change that advances the chain beyond the submit
// confidence
currentEpoch = 1 + SubmitConfidence
go triggerHeadAdvance(t, s, currentEpoch+ChallengeConfidence)
// Should be no change to state yet
<-s.ch.proveHdlr.processedHeadChanges
require.Equal(t, postStatusProving, s.mock.getPostStatus(di))
// Send a response to the call to generate proofs
posts := []minertypes.SubmitWindowedPoStParams{{Deadline: di.Index}}
mock.proveResult <- &proveRes{posts: posts}
// Should move to proving complete
<-s.ch.proveHdlr.processedPostResults
di = mock.getDeadline(currentEpoch)
require.Equal(t, postStatusComplete, s.mock.getPostStatus(di))
// Trigger head change that advances the chain to the Challenge epoch for
// the next deadline
go func() {
di = NextDeadline(di)
currentEpoch = di.Challenge + ChallengeConfidence
triggerHeadAdvance(t, s, currentEpoch)
}()
// Should start generating next window's proof
<-s.ch.proveHdlr.processedHeadChanges
require.Equal(t, postStatusProving, s.mock.getPostStatus(di))
}
// TestChangeHandlerProvingRounds verifies we can generate several rounds of
// proofs as the chain head advances
func TestChangeHandlerProvingRounds(t *testing.T) {
//stm: @WDPOST_CHANGE_HANDLER_START_001, @WDPOST_CHANGE_HANDLER_UPDATE_001
//stm: @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_001, @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_PW_001
//stm: @WDPOST_SUBMIT_HANDLER_SUBMIT_IF_READY_002, @WDPOST_SUBMIT_HANDLER_SUBMIT_IF_READY_003, @WDPOST_SUBMIT_HANDLER_SUBMIT_IF_READY_005
//stm: @WDPOST_SUBMIT_HANDLER_SUBMIT_IF_READY_004, @WDPOST_PROVE_HANDLER_PROCESS_POST_RESULT_001
s := makeScaffolding(t)
mock := s.mock
defer s.ch.shutdown()
s.ch.start()
completeProofIndex := abi.ChainEpoch(10)
for currentEpoch := abi.ChainEpoch(1); currentEpoch < minertypes.WPoStChallengeWindow*5; currentEpoch++ {
// Trigger a head change
di := mock.getDeadline(currentEpoch)
go triggerHeadAdvance(t, s, currentEpoch+ChallengeConfidence)
// Wait for prover to process head change
<-s.ch.proveHdlr.processedHeadChanges
completeProofEpoch := di.Open + completeProofIndex
next := NextDeadline(di)
//fmt.Println("epoch", currentEpoch, s.mock.getPostStatus(di), "next", s.mock.getPostStatus(next))
if currentEpoch >= next.Challenge {
require.Equal(t, postStatusComplete, s.mock.getPostStatus(di))
// At the next deadline's challenge epoch, should start proving
// for that epoch
require.Equal(t, postStatusProving, s.mock.getPostStatus(next))
} else if currentEpoch > completeProofEpoch {
// After proving for the round is complete, should be in complete state
require.Equal(t, postStatusComplete, s.mock.getPostStatus(di))
require.Equal(t, postStatusStart, s.mock.getPostStatus(next))
} else {
// Until proving completes, should be in the proving state
require.Equal(t, postStatusProving, s.mock.getPostStatus(di))
require.Equal(t, postStatusStart, s.mock.getPostStatus(next))
}
// Wait for submitter to process head change
<-s.ch.submitHdlr.processedHeadChanges
completeSubmitEpoch := completeProofEpoch + 1
//fmt.Println("epoch", currentEpoch, s.submitState(di))
if currentEpoch > completeSubmitEpoch {
require.Equal(t, SubmitStateComplete, s.submitState(di))
} else if currentEpoch > completeProofEpoch {
require.Equal(t, SubmitStateSubmitting, s.submitState(di))
} else {
require.Equal(t, SubmitStateStart, s.submitState(di))
}
if currentEpoch == completeProofEpoch {
// Send a response to the call to generate proofs
posts := []minertypes.SubmitWindowedPoStParams{{Deadline: di.Index}}
mock.proveResult <- &proveRes{posts: posts}
// Should move to proving complete
<-s.ch.proveHdlr.processedPostResults
require.Equal(t, postStatusComplete, s.mock.getPostStatus(di))
}
if currentEpoch == completeSubmitEpoch {
// Send a response to the submit call
mock.submitResult <- nil
// Should move to the complete state
<-s.ch.submitHdlr.processedSubmitResults
require.Equal(t, SubmitStateComplete, s.submitState(di))
}
}
}
// TestChangeHandlerProvingErrorRecovery verifies that the proof handler
// recovers correctly from an error
func TestChangeHandlerProvingErrorRecovery(t *testing.T) {
//stm: @WDPOST_CHANGE_HANDLER_START_001, @WDPOST_CHANGE_HANDLER_UPDATE_001
//stm: @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_001, @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_PW_001
//stm: @WDPOST_SUBMIT_HANDLER_SUBMIT_IF_READY_004, @WDPOST_PROVE_HANDLER_PROCESS_POST_RESULT_001
s := makeScaffolding(t)
mock := s.mock
defer s.ch.shutdown()
s.ch.start()
// Trigger a head change
currentEpoch := abi.ChainEpoch(1)
go triggerHeadAdvance(t, s, currentEpoch)
// Should start proving
<-s.ch.proveHdlr.processedHeadChanges
di := mock.getDeadline(currentEpoch)
require.Equal(t, postStatusProving, s.mock.getPostStatus(di))
// Send an error response to the call to generate proofs
mock.proveResult <- &proveRes{err: fmt.Errorf("err")}
// Should abort and then move to start state
<-s.ch.proveHdlr.processedPostResults
require.Equal(t, postStatusStart, s.mock.getPostStatus(di))
// Trigger a head change
go triggerHeadAdvance(t, s, currentEpoch)
// Should start proving
<-s.ch.proveHdlr.processedHeadChanges
require.Equal(t, postStatusProving, s.mock.getPostStatus(di))
// Send a success response to the call to generate proofs
posts := []minertypes.SubmitWindowedPoStParams{{Deadline: di.Index}}
mock.proveResult <- &proveRes{posts: posts}
// Should move to proving complete
<-s.ch.proveHdlr.processedPostResults
require.Equal(t, postStatusComplete, s.mock.getPostStatus(di))
}
// TestChangeHandlerSubmitErrorRecovery verifies that the submit handler
// recovers correctly from an error
func TestChangeHandlerSubmitErrorRecovery(t *testing.T) {
//stm: @WDPOST_CHANGE_HANDLER_START_001, @WDPOST_CHANGE_HANDLER_UPDATE_001
//stm: @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_001, @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_PW_001
//stm: @WDPOST_SUBMIT_HANDLER_SUBMIT_IF_READY_004, @WDPOST_PROVE_HANDLER_PROCESS_POST_RESULT_001
//stm: @WDPOST_SUBMIT_HANDLER_PROCESS_PROCESS_RESULTS_001
s := makeScaffolding(t)
mock := s.mock
defer s.ch.shutdown()
s.ch.start()
// Trigger a head change
currentEpoch := abi.ChainEpoch(1)
go triggerHeadAdvance(t, s, currentEpoch)
// Should start proving
<-s.ch.proveHdlr.processedHeadChanges
di := mock.getDeadline(currentEpoch)
require.Equal(t, postStatusProving, s.mock.getPostStatus(di))
// Submitter doesn't have anything to do yet
<-s.ch.submitHdlr.processedHeadChanges
require.Equal(t, SubmitStateStart, s.submitState(di))
// Send a response to the call to generate proofs
posts := []minertypes.SubmitWindowedPoStParams{{Deadline: di.Index}}
mock.proveResult <- &proveRes{posts: posts}
// Should move to proving complete
<-s.ch.proveHdlr.processedPostResults
require.Equal(t, postStatusComplete, s.mock.getPostStatus(di))
// Move to the correct height to submit the proof
currentEpoch = 1 + SubmitConfidence
go triggerHeadAdvance(t, s, currentEpoch)
// Read from prover incoming channel (so as not to block)
<-s.ch.proveHdlr.processedHeadChanges
// Should move to submitting state
<-s.ch.submitHdlr.processedHeadChanges
di = mock.getDeadline(currentEpoch)
require.Equal(t, SubmitStateSubmitting, s.submitState(di))
// Send an error response to the call to submit
mock.submitResult <- fmt.Errorf("err")
// Should abort and then move back to the start state
<-s.ch.submitHdlr.processedSubmitResults
require.Equal(t, SubmitStateStart, s.submitState(di))
require.True(t, mock.wasAbortCalled())
// Trigger another head change
go triggerHeadAdvance(t, s, currentEpoch)
// Read from prover incoming channel (so as not to block)
<-s.ch.proveHdlr.processedHeadChanges
// Should move to submitting state
<-s.ch.submitHdlr.processedHeadChanges
di = mock.getDeadline(currentEpoch)
require.Equal(t, SubmitStateSubmitting, s.submitState(di))
// Send a response to the submit call
mock.submitResult <- nil
// Should move to the complete state
<-s.ch.submitHdlr.processedSubmitResults
require.Equal(t, SubmitStateComplete, s.submitState(di))
}
// TestChangeHandlerProveExpiry verifies that the prove handler
// behaves correctly on expiry
func TestChangeHandlerProveExpiry(t *testing.T) {
//stm: @WDPOST_CHANGE_HANDLER_START_001, @WDPOST_CHANGE_HANDLER_UPDATE_001
//stm: @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_001, @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_PW_001
//stm: @WDPOST_SUBMIT_HANDLER_SUBMIT_IF_READY_004, @WDPOST_PROVE_HANDLER_PROCESS_POST_RESULT_001
s := makeScaffolding(t)
mock := s.mock
defer s.ch.shutdown()
s.ch.start()
// Trigger a head change
currentEpoch := abi.ChainEpoch(1)
go triggerHeadAdvance(t, s, currentEpoch)
// Should start proving
<-s.ch.proveHdlr.processedHeadChanges
di := mock.getDeadline(currentEpoch)
require.Equal(t, postStatusProving, s.mock.getPostStatus(di))
// Move to a height that expires the current proof
currentEpoch = minertypes.WPoStChallengeWindow
di = mock.getDeadline(currentEpoch)
go triggerHeadAdvance(t, s, currentEpoch)
// Should trigger an abort and start proving for the new deadline
<-s.ch.proveHdlr.processedHeadChanges
require.Equal(t, postStatusProving, s.mock.getPostStatus(di))
<-s.ch.proveHdlr.processedPostResults
require.True(t, mock.wasAbortCalled())
// Send a response to the call to generate proofs
posts := []minertypes.SubmitWindowedPoStParams{{Deadline: di.Index}}
mock.proveResult <- &proveRes{posts: posts}
// Should move to proving complete
<-s.ch.proveHdlr.processedPostResults
require.Equal(t, postStatusComplete, s.mock.getPostStatus(di))
}
// TestChangeHandlerSubmitExpiry verifies that the submit handler
// behaves correctly on expiry
func TestChangeHandlerSubmitExpiry(t *testing.T) {
//stm: @WDPOST_CHANGE_HANDLER_START_001, @WDPOST_CHANGE_HANDLER_UPDATE_001
//stm: @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_001, @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_PW_001
//stm: @WDPOST_SUBMIT_HANDLER_SUBMIT_IF_READY_004, @WDPOST_SUBMIT_HANDLER_SUBMIT_IF_READY_002, @WDPOST_PROVE_HANDLER_PROCESS_POST_RESULT_001
s := makeScaffolding(t)
mock := s.mock
// Ignore prove handler head change processing for this test
s.ch.proveHdlr.processedHeadChanges = nil
defer s.ch.shutdown()
s.ch.start()
// Trigger a head change
currentEpoch := abi.ChainEpoch(1)
go triggerHeadAdvance(t, s, currentEpoch)
// Submitter doesn't have anything to do yet
<-s.ch.submitHdlr.processedHeadChanges
di := mock.getDeadline(currentEpoch)
require.Equal(t, SubmitStateStart, s.submitState(di))
// Send a response to the call to generate proofs
posts := []minertypes.SubmitWindowedPoStParams{{Deadline: di.Index}}
mock.proveResult <- &proveRes{posts: posts}
// Should move to proving complete
<-s.ch.proveHdlr.processedPostResults
require.Equal(t, postStatusComplete, s.mock.getPostStatus(di))
// Move to the correct height to submit the proof
currentEpoch = 1 + SubmitConfidence
go triggerHeadAdvance(t, s, currentEpoch)
// Should move to submitting state
<-s.ch.submitHdlr.processedHeadChanges
di = mock.getDeadline(currentEpoch)
require.Equal(t, SubmitStateSubmitting, s.submitState(di))
// Move to a height that expires the submit
currentEpoch = minertypes.WPoStChallengeWindow
di = mock.getDeadline(currentEpoch)
go triggerHeadAdvance(t, s, currentEpoch)
// Should trigger an abort and move back to start state
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
<-s.ch.submitHdlr.processedSubmitResults
require.True(t, mock.wasAbortCalled())
}()
go func() {
defer wg.Done()
<-s.ch.submitHdlr.processedHeadChanges
require.Equal(t, SubmitStateStart, s.submitState(di))
}()
wg.Wait()
}
// TestChangeHandlerProveRevert verifies that the prove handler
// behaves correctly on revert
func TestChangeHandlerProveRevert(t *testing.T) {
//stm: @WDPOST_CHANGE_HANDLER_START_001, @WDPOST_CHANGE_HANDLER_UPDATE_001
//stm: @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_001, @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_PW_001
//stm: @WDPOST_SUBMIT_HANDLER_SUBMIT_IF_READY_004, @WDPOST_PROVE_HANDLER_PROCESS_POST_RESULT_001
s := makeScaffolding(t)
mock := s.mock
defer s.ch.shutdown()
s.ch.start()
// Trigger a head change
currentEpoch := minertypes.WPoStChallengeWindow
go triggerHeadAdvance(t, s, currentEpoch)
// Should start proving
<-s.ch.proveHdlr.processedHeadChanges
di := mock.getDeadline(currentEpoch)
require.Equal(t, postStatusProving, s.mock.getPostStatus(di))
// Trigger a revert to the previous epoch
revertEpoch := di.Open - 5
go triggerHeadChange(t, s, revertEpoch, currentEpoch)
// Should be no change
<-s.ch.proveHdlr.processedHeadChanges
require.Equal(t, postStatusProving, s.mock.getPostStatus(di))
// Send a response to the call to generate proofs
posts := []minertypes.SubmitWindowedPoStParams{{Deadline: di.Index}}
mock.proveResult <- &proveRes{posts: posts}
// Should move to proving complete
<-s.ch.proveHdlr.processedPostResults
require.Equal(t, postStatusComplete, s.mock.getPostStatus(di))
require.False(t, mock.wasAbortCalled())
}
// TestChangeHandlerSubmittingRevert verifies that the submit handler
// behaves correctly when there's a revert from the submitting state
func TestChangeHandlerSubmittingRevert(t *testing.T) {
//stm: @WDPOST_CHANGE_HANDLER_START_001, @WDPOST_CHANGE_HANDLER_UPDATE_001
//stm: @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_001, @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_PW_001
//stm: @WDPOST_SUBMIT_HANDLER_SUBMIT_IF_READY_004, @WDPOST_PROVE_HANDLER_PROCESS_POST_RESULT_001
//stm: @WDPOST_SUBMIT_HANDLER_PROCESS_PROCESS_RESULTS_001
s := makeScaffolding(t)
mock := s.mock
// Ignore prove handler head change processing for this test
s.ch.proveHdlr.processedHeadChanges = nil
defer s.ch.shutdown()
s.ch.start()
// Trigger a head change
currentEpoch := minertypes.WPoStChallengeWindow
go triggerHeadAdvance(t, s, currentEpoch)
// Submitter doesn't have anything to do yet
<-s.ch.submitHdlr.processedHeadChanges
di := mock.getDeadline(currentEpoch)
require.Equal(t, SubmitStateStart, s.submitState(di))
// Send a response to the call to generate proofs
posts := []minertypes.SubmitWindowedPoStParams{{Deadline: di.Index}}
mock.proveResult <- &proveRes{posts: posts}
// Should move to proving complete
<-s.ch.proveHdlr.processedPostResults
require.Equal(t, postStatusComplete, s.mock.getPostStatus(di))
// Move to the correct height to submit the proof
currentEpoch = currentEpoch + 1 + SubmitConfidence
go triggerHeadAdvance(t, s, currentEpoch)
// Should move to submitting state
<-s.ch.submitHdlr.processedHeadChanges
di = mock.getDeadline(currentEpoch)
require.Equal(t, SubmitStateSubmitting, s.submitState(di))
// Trigger a revert to the previous epoch
revertEpoch := di.Open - 5
go triggerHeadChange(t, s, revertEpoch, currentEpoch)
var wg sync.WaitGroup
wg.Add(2)
// Should trigger an abort
go func() {
defer wg.Done()
<-s.ch.submitHdlr.processedSubmitResults
require.True(t, mock.wasAbortCalled())
}()
// Should resubmit current epoch
go func() {
defer wg.Done()
<-s.ch.submitHdlr.processedHeadChanges
require.Equal(t, SubmitStateSubmitting, s.submitState(di))
}()
wg.Wait()
// Send a response to the resubmit call
mock.submitResult <- nil
// Should move to the complete state
<-s.ch.submitHdlr.processedSubmitResults
require.Equal(t, SubmitStateComplete, s.submitState(di))
}
// TestChangeHandlerSubmitCompleteRevert verifies that the submit handler
// behaves correctly when there's a revert from the submit complete state
func TestChangeHandlerSubmitCompleteRevert(t *testing.T) {
//stm: @WDPOST_CHANGE_HANDLER_START_001, @WDPOST_CHANGE_HANDLER_UPDATE_001
//stm: @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_001, @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_PW_001
//stm: @WDPOST_SUBMIT_HANDLER_SUBMIT_IF_READY_004, @WDPOST_PROVE_HANDLER_PROCESS_POST_RESULT_001
//stm: @WDPOST_SUBMIT_HANDLER_PROCESS_PROCESS_RESULTS_001
s := makeScaffolding(t)
mock := s.mock
// Ignore prove handler head change processing for this test
s.ch.proveHdlr.processedHeadChanges = nil
defer s.ch.shutdown()
s.ch.start()
// Trigger a head change
currentEpoch := minertypes.WPoStChallengeWindow
go triggerHeadAdvance(t, s, currentEpoch)
// Submitter doesn't have anything to do yet
<-s.ch.submitHdlr.processedHeadChanges
di := mock.getDeadline(currentEpoch)
require.Equal(t, SubmitStateStart, s.submitState(di))
// Send a response to the call to generate proofs
posts := []minertypes.SubmitWindowedPoStParams{{Deadline: di.Index}}
mock.proveResult <- &proveRes{posts: posts}
// Should move to proving complete
<-s.ch.proveHdlr.processedPostResults
require.Equal(t, postStatusComplete, s.mock.getPostStatus(di))
// Move to the correct height to submit the proof
currentEpoch = currentEpoch + 1 + SubmitConfidence
go triggerHeadAdvance(t, s, currentEpoch)
// Should move to submitting state
<-s.ch.submitHdlr.processedHeadChanges
di = mock.getDeadline(currentEpoch)
require.Equal(t, SubmitStateSubmitting, s.submitState(di))
// Send a response to the resubmit call
mock.submitResult <- nil
// Should move to the complete state
<-s.ch.submitHdlr.processedSubmitResults
require.Equal(t, SubmitStateComplete, s.submitState(di))
// Trigger a revert to the previous epoch
revertEpoch := di.Open - 5
go triggerHeadChange(t, s, revertEpoch, currentEpoch)
// Should resubmit current epoch
<-s.ch.submitHdlr.processedHeadChanges
require.Equal(t, SubmitStateSubmitting, s.submitState(di))
// Send a response to the resubmit call
mock.submitResult <- nil
// Should move to the complete state
<-s.ch.submitHdlr.processedSubmitResults
require.Equal(t, SubmitStateComplete, s.submitState(di))
}
// TestChangeHandlerSubmitRevertTwoEpochs verifies that the submit handler
// behaves correctly when the revert is two epochs deep
func TestChangeHandlerSubmitRevertTwoEpochs(t *testing.T) {
//stm: @WDPOST_CHANGE_HANDLER_START_001, @WDPOST_CHANGE_HANDLER_UPDATE_001
//stm: @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_001, @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_PW_001
//stm: @WDPOST_SUBMIT_HANDLER_SUBMIT_IF_READY_004, @WDPOST_SUBMIT_HANDLER_SUBMIT_IF_READY_002, @WDPOST_PROVE_HANDLER_PROCESS_POST_RESULT_001
//stm: @WDPOST_SUBMIT_HANDLER_PROCESS_PROCESS_RESULTS_001
s := makeScaffolding(t)
mock := s.mock
// Ignore prove handler head change processing for this test
s.ch.proveHdlr.processedHeadChanges = nil
defer s.ch.shutdown()
s.ch.start()
// Trigger a head change
currentEpoch := minertypes.WPoStChallengeWindow
go triggerHeadAdvance(t, s, currentEpoch)
// Submitter doesn't have anything to do yet
<-s.ch.submitHdlr.processedHeadChanges
diE1 := mock.getDeadline(currentEpoch)
require.Equal(t, SubmitStateStart, s.submitState(diE1))
// Send a response to the call to generate proofs
posts := []minertypes.SubmitWindowedPoStParams{{Deadline: diE1.Index}}
mock.proveResult <- &proveRes{posts: posts}
// Should move to proving complete
<-s.ch.proveHdlr.processedPostResults
require.Equal(t, postStatusComplete, s.mock.getPostStatus(diE1))
// Move to the challenge epoch for the next deadline
diE2 := NextDeadline(diE1)
currentEpoch = diE2.Challenge + ChallengeConfidence
go triggerHeadAdvance(t, s, currentEpoch)
// Should move to submitting state for epoch 1
<-s.ch.submitHdlr.processedHeadChanges
diE1 = mock.getDeadline(currentEpoch)
require.Equal(t, SubmitStateSubmitting, s.submitState(diE1))
// Send a response to the submit call for epoch 1
mock.submitResult <- nil
// Should move to the complete state for epoch 1
<-s.ch.submitHdlr.processedSubmitResults
require.Equal(t, SubmitStateComplete, s.submitState(diE1))
// Should start proving epoch 2
// Send a response to the call to generate proofs
postsE2 := []minertypes.SubmitWindowedPoStParams{{Deadline: diE2.Index}}
mock.proveResult <- &proveRes{posts: postsE2}
// Should move to proving complete for epoch 2
<-s.ch.proveHdlr.processedPostResults
require.Equal(t, postStatusComplete, s.mock.getPostStatus(diE2))
// Move to the correct height to submit the proof for epoch 2
currentEpoch = diE2.Open + 1 + SubmitConfidence
go triggerHeadAdvance(t, s, currentEpoch)
// Should move to submitting state for epoch 2
<-s.ch.submitHdlr.processedHeadChanges
diE2 = mock.getDeadline(currentEpoch)
require.Equal(t, SubmitStateSubmitting, s.submitState(diE2))
// Trigger a revert through two epochs (from epoch 2 to epoch 0)
revertEpoch := diE1.Open - 5
go triggerHeadChange(t, s, revertEpoch, currentEpoch)
var wg sync.WaitGroup
wg.Add(2)
// Should trigger an abort
go func() {
defer wg.Done()
<-s.ch.submitHdlr.processedSubmitResults
require.True(t, mock.wasAbortCalled())
}()
go func() {
defer wg.Done()
<-s.ch.submitHdlr.processedHeadChanges
// Should reset epoch 1 (that is expired) to start state
require.Equal(t, SubmitStateStart, s.submitState(diE1))
// Should resubmit epoch 2
require.Equal(t, SubmitStateSubmitting, s.submitState(diE2))
}()
wg.Wait()
// Send a response to the resubmit call for epoch 2
mock.submitResult <- nil
// Should move to the complete state for epoch 2
<-s.ch.submitHdlr.processedSubmitResults
require.Equal(t, SubmitStateComplete, s.submitState(diE2))
}
// TestChangeHandlerSubmitRevertAdvanceLess verifies that the submit handler
// behaves correctly when the revert is two epochs deep and the advance is
// to a lower height than before
func TestChangeHandlerSubmitRevertAdvanceLess(t *testing.T) {
//stm: @WDPOST_CHANGE_HANDLER_START_001, @WDPOST_CHANGE_HANDLER_UPDATE_001
//stm: @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_001, @WDPOST_SUBMIT_HANDLER_PROCESS_HEAD_CHANGE_PW_001
//stm: @WDPOST_SUBMIT_HANDLER_SUBMIT_IF_READY_004, @WDPOST_SUBMIT_HANDLER_SUBMIT_IF_READY_002, @WDPOST_PROVE_HANDLER_PROCESS_POST_RESULT_001
//stm: @WDPOST_SUBMIT_HANDLER_PROCESS_PROCESS_RESULTS_001
s := makeScaffolding(t)
mock := s.mock
// Ignore prove handler head change processing for this test
s.ch.proveHdlr.processedHeadChanges = nil
defer s.ch.shutdown()
s.ch.start()
// Trigger a head change
currentEpoch := minertypes.WPoStChallengeWindow
go triggerHeadAdvance(t, s, currentEpoch)
// Submitter doesn't have anything to do yet
<-s.ch.submitHdlr.processedHeadChanges
diE1 := mock.getDeadline(currentEpoch)
require.Equal(t, SubmitStateStart, s.submitState(diE1))
// Send a response to the call to generate proofs
posts := []minertypes.SubmitWindowedPoStParams{{Deadline: diE1.Index}}
mock.proveResult <- &proveRes{posts: posts}
// Should move to proving complete
<-s.ch.proveHdlr.processedPostResults
require.Equal(t, postStatusComplete, s.mock.getPostStatus(diE1))
// Move to the challenge epoch for the next deadline
diE2 := NextDeadline(diE1)
currentEpoch = diE2.Challenge + ChallengeConfidence
go triggerHeadAdvance(t, s, currentEpoch)
// Should move to submitting state for epoch 1
<-s.ch.submitHdlr.processedHeadChanges
diE1 = mock.getDeadline(currentEpoch)
require.Equal(t, SubmitStateSubmitting, s.submitState(diE1))
// Send a response to the submit call for epoch 1
mock.submitResult <- nil
// Should move to the complete state for epoch 1
<-s.ch.submitHdlr.processedSubmitResults
require.Equal(t, SubmitStateComplete, s.submitState(diE1))
// Should start proving epoch 2
// Send a response to the call to generate proofs
postsE2 := []minertypes.SubmitWindowedPoStParams{{Deadline: diE2.Index}}
mock.proveResult <- &proveRes{posts: postsE2}
// Should move to proving complete for epoch 2
<-s.ch.proveHdlr.processedPostResults
require.Equal(t, postStatusComplete, s.mock.getPostStatus(diE2))
// Move to the correct height to submit the proof for epoch 2
currentEpoch = diE2.Open + 1 + SubmitConfidence
go triggerHeadAdvance(t, s, currentEpoch)
// Should move to submitting state for epoch 2
<-s.ch.submitHdlr.processedHeadChanges
diE2 = mock.getDeadline(currentEpoch)
require.Equal(t, SubmitStateSubmitting, s.submitState(diE2))
// Trigger a revert through two epochs (from epoch 2 to epoch 0)
// then advance to the previous epoch (to epoch 1)
revertEpoch := diE1.Open - 5
currentEpoch = diE2.Open - 1
go triggerHeadChange(t, s, revertEpoch, currentEpoch)
var wg sync.WaitGroup
wg.Add(2)
// Should trigger an abort
go func() {
defer wg.Done()
<-s.ch.submitHdlr.processedSubmitResults
require.True(t, mock.wasAbortCalled())
}()
go func() {
defer wg.Done()
<-s.ch.submitHdlr.processedHeadChanges
// Should resubmit epoch 1
require.Equal(t, SubmitStateSubmitting, s.submitState(diE1))
// Should reset epoch 2 to start state
require.Equal(t, SubmitStateStart, s.submitState(diE2))
}()
wg.Wait()
// Send a response to the resubmit call for epoch 1
mock.submitResult <- nil
// Should move to the complete state for epoch 1
<-s.ch.submitHdlr.processedSubmitResults
require.Equal(t, SubmitStateComplete, s.submitState(diE1))
}
type smScaffolding struct {
ctx context.Context
mock *mockAPI
ch *changeHandler
}
func makeScaffolding(t *testing.T) *smScaffolding {
ctx := context.Background()
actor := tutils.NewActorAddr(t, "actor")
mock := newMockAPI()
ch := newChangeHandler(mock, actor)
mock.setChangeHandler(ch)
ch.proveHdlr.processedHeadChanges = make(chan *headChange)
ch.proveHdlr.processedPostResults = make(chan *postResult)
ch.submitHdlr.processedHeadChanges = make(chan *headChange)
ch.submitHdlr.processedSubmitResults = make(chan *submitResult)
return &smScaffolding{
ctx: ctx,
mock: mock,
ch: ch,
}
}
func triggerHeadAdvance(t *testing.T, s *smScaffolding, height abi.ChainEpoch) {
ts := s.mock.makeTs(t, height)
err := s.ch.update(s.ctx, nil, ts)
require.NoError(t, err)
}
func triggerHeadChange(t *testing.T, s *smScaffolding, revertHeight, advanceHeight abi.ChainEpoch) {
tsRev := s.mock.makeTs(t, revertHeight)
tsAdv := s.mock.makeTs(t, advanceHeight)
err := s.ch.update(s.ctx, tsRev, tsAdv)
require.NoError(t, err)
}
func (s *smScaffolding) submitState(di *dline.Info) SubmitState {
return s.ch.submitHdlr.getPostWindow(di).submitState
}
func makeTs(t *testing.T, h abi.ChainEpoch) *types.TipSet {
var parents []cid.Cid
msgcid := dummyCid
a, _ := address.NewFromString("t00")
b, _ := address.NewFromString("t02")
var ts, err = types.NewTipSet([]*types.BlockHeader{
{
Height: h,
Miner: a,
Parents: parents,
Ticket: &types.Ticket{VRFProof: []byte{byte(h % 2)}},
ParentStateRoot: dummyCid,
Messages: msgcid,
ParentMessageReceipts: dummyCid,
BlockSig: &crypto.Signature{Type: crypto.SigTypeBLS},
BLSAggregate: &crypto.Signature{Type: crypto.SigTypeBLS},
},
{
Height: h,
Miner: b,
Parents: parents,
Ticket: &types.Ticket{VRFProof: []byte{byte((h + 1) % 2)}},
ParentStateRoot: dummyCid,
Messages: msgcid,
ParentMessageReceipts: dummyCid,
BlockSig: &crypto.Signature{Type: crypto.SigTypeBLS},
BLSAggregate: &crypto.Signature{Type: crypto.SigTypeBLS},
},
})
require.NoError(t, err)
return ts
}