Skip to content

Commit

Permalink
implement miner.SlashStorageFault (#3065)
Browse files Browse the repository at this point in the history
* implement miner.SlashStorageFault

* add test for uninitialized intset values

* set owedStorageCollateral to zero until we know the correct amount

* remove nil check from intset

* fix miner redeem test

* remove test for removed functionality
  • Loading branch information
acruikshank authored Jul 16, 2019
1 parent e676cf6 commit 1dee904
Show file tree
Hide file tree
Showing 5 changed files with 248 additions and 24 deletions.
83 changes: 83 additions & 0 deletions actor/builtin/miner/miner.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ const (
ErrGetProofsModeFailed = 42
// ErrInsufficientCollateral indicates that the miner does not have sufficient collateral to commit additional sectors.
ErrInsufficientCollateral = 43
// ErrMinerAlreadySlashed indicates that an attempt has been made to slash an already slashed miner
ErrMinerAlreadySlashed = 44
// ErrMinerNotSlashable indicates that an attempt has been made to slash a miner that does not meet the criteria for slashing.
ErrMinerNotSlashable = 45
)

// Errors map error codes to revert errors this actor may return.
Expand Down Expand Up @@ -177,6 +181,16 @@ type State struct {
// SectorSize is the amount of space in each sector committed to the network
// by this miner.
SectorSize *types.BytesAmount

// SlashedSet is a set of sector ids that have been slashed
SlashedSet types.IntSet

// SlashedAt is the time at which this miner was slashed
SlashedAt *types.BlockHeight

// OwedStorageCollateral is the collateral for sectors that have been slashed.
// This collateral can be collected from arbitrated deals, but not de-pledged.
OwedStorageCollateral types.AttoFIL
}

// NewActor returns a new miner actor with the provided balance.
Expand All @@ -196,6 +210,7 @@ func NewState(owner, worker address.Address, pid peer.ID, sectorSize *types.Byte
Power: types.NewBytesAmount(0),
NextAskID: big.NewInt(0),
SectorSize: sectorSize,
SlashedSet: types.EmptyIntSet(),
ActiveCollateral: types.ZeroAttoFIL,
}
}
Expand Down Expand Up @@ -256,6 +271,10 @@ var minerExports = exec.Exports{
Params: []abi.Type{abi.PoStProofs, abi.IntSet},
Return: []abi.Type{},
},
"slashStorageFault": &exec.FunctionSignature{
Params: []abi.Type{},
Return: []abi.Type{},
},
"changeWorker": &exec.FunctionSignature{
Params: []abi.Type{abi.Address},
Return: []abi.Type{},
Expand Down Expand Up @@ -955,6 +974,70 @@ func (ma *Actor) SubmitPoSt(ctx exec.VMContext, poStProofs []types.PoStProof, do
return 0, nil
}

// SlashStorageFault is called by an independent actor to remove power and
// take collateral from this miner when the miner has failed to submit a
// PoSt on time.
func (ma *Actor) SlashStorageFault(ctx exec.VMContext) (uint8, error) {
if err := ctx.Charge(actor.DefaultGasCost); err != nil {
return exec.ErrInsufficientGas, errors.RevertErrorWrap(err, "Insufficient gas")
}

chainHeight := ctx.BlockHeight()
var state State
_, err := actor.WithState(ctx, &state, func() (interface{}, error) {
// You can only be slashed once for missing your PoSt.
if state.SlashedAt != nil {
return nil, errors.NewCodedRevertError(ErrMinerAlreadySlashed, "miner already slashed")
}

// Only a miner who is expected to prove, can be slashed.
if state.ProvingSet.Size() == 0 {
return nil, errors.NewCodedRevertError(ErrMinerNotSlashable, "miner is inactive")
}

// Only if the miner is actually late, they can be slashed.
deadline := state.ProvingPeriodEnd.Add(GenerationAttackTime(state.SectorSize))
if chainHeight.LessEqual(deadline) {
return nil, errors.NewCodedRevertError(ErrMinerNotSlashable, "miner not yet tardy")
}

// Strip the miner of their power.
powerDelta := types.ZeroBytes.Sub(state.Power) // negate bytes amount
_, ret, err := ctx.Send(address.StorageMarketAddress, "updateStorage", types.ZeroAttoFIL, []interface{}{powerDelta})
if err != nil {
return nil, err
}
if ret != 0 {
return nil, Errors[ErrStoragemarketCallFailed]
}
state.Power = types.NewBytesAmount(0)

// record what has been slashed
state.SlashedSet = state.ProvingSet

// reserve collateral for arbitration
// TODO: We currently do not know the correct amount of collateral to reserve here: https://github.com/filecoin-project/go-filecoin/issues/3050
state.OwedStorageCollateral = types.ZeroAttoFIL

// remove proving set from our sectors
state.SectorCommitments.Drop(state.SlashedSet.Values())

// clear proving set
state.ProvingSet = types.NewIntSet()

// save chain height, so we know when this miner was slashed
state.SlashedAt = chainHeight

return nil, nil
})

if err != nil {
return errors.CodeError(err), err
}

return 0, nil
}

// GetProvingPeriod returns the proving period start and proving period end
func (ma *Actor) GetProvingPeriod(ctx exec.VMContext) (*types.BlockHeight, *types.BlockHeight, uint8, error) {
var state State
Expand Down
156 changes: 151 additions & 5 deletions actor/builtin/miner/miner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import (
"math/big"
"testing"

"github.com/filecoin-project/go-filecoin/exec"

cbor "github.com/ipfs/go-ipld-cbor"
"github.com/libp2p/go-libp2p-peer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand All @@ -17,6 +16,7 @@ import (
. "github.com/filecoin-project/go-filecoin/actor/builtin/miner"
"github.com/filecoin-project/go-filecoin/address"
"github.com/filecoin-project/go-filecoin/consensus"
"github.com/filecoin-project/go-filecoin/exec"
"github.com/filecoin-project/go-filecoin/state"
th "github.com/filecoin-project/go-filecoin/testhelpers"
tf "github.com/filecoin-project/go-filecoin/testhelpers/testflags"
Expand Down Expand Up @@ -852,7 +852,6 @@ func TestMinerSubmitPoStNextDoneSet(t *testing.T) {
failingDone := done.Add(uint64(30))
mal.assertPoStFail(secondProvingPeriodStart+5, failingDone, uint8(ErrInvalidSector))
})

}

func TestMinerSubmitPoSt(t *testing.T) {
Expand Down Expand Up @@ -938,6 +937,138 @@ func TestMinerSubmitPoSt(t *testing.T) {
})
}

func TestActorSlashStorageFault(t *testing.T) {
tf.UnitTest(t)

firstCommitBlockHeight := uint64(3)
secondProvingPeriodStart := firstCommitBlockHeight + ProvingPeriodDuration(types.OneKiBSectorSize)
thirdProvingPeriodStart := secondProvingPeriodStart + ProvingPeriodDuration(types.OneKiBSectorSize)
thirdProvingPeriodEnd := thirdProvingPeriodStart + ProvingPeriodDuration(types.OneKiBSectorSize)
lastPossibleSubmission := thirdProvingPeriodEnd + LargestSectorGenerationAttackThresholdBlocks

// CreateTestMiner creates a new test miner with the given peerID and miner
// owner address and a given number of committed sectors
createMinerWithPower := func(t *testing.T) (state.Tree, vm.StorageMap, address.Address) {
ctx := context.Background()
st, vms := th.RequireCreateStorages(ctx, t)
minerAddr := th.CreateTestMiner(t, st, vms, address.TestAddress, th.RequireRandomPeerID(t))

ancestors := th.RequireTipSetChain(t, 10)
proof := th.MakeRandomPoStProofForTest()
doneDefault := types.EmptyIntSet()

// add a sector
_, err := th.CreateAndApplyTestMessage(t, st, vms, minerAddr, 0, firstCommitBlockHeight, "commitSector", ancestors, uint64(1), th.MakeCommitment(), th.MakeCommitment(), th.MakeCommitment(), th.MakeRandomBytes(types.TwoPoRepProofPartitions.ProofLen()))
require.NoError(t, err)

// add another sector (not in proving set yet)
_, err = th.CreateAndApplyTestMessage(t, st, vms, minerAddr, 0, firstCommitBlockHeight+1, "commitSector", ancestors, uint64(2), th.MakeCommitment(), th.MakeCommitment(), th.MakeCommitment(), th.MakeRandomBytes(types.TwoPoRepProofPartitions.ProofLen()))
require.NoError(t, err)

// submit post (first sector only)
_, err = th.CreateAndApplyTestMessage(t, st, vms, minerAddr, 0, secondProvingPeriodStart, "submitPoSt", ancestors, []types.PoStProof{proof}, doneDefault)
require.NoError(t, err)

// submit post (both sectors
_, err = th.CreateAndApplyTestMessage(t, st, vms, minerAddr, 0, thirdProvingPeriodStart, "submitPoSt", ancestors, []types.PoStProof{proof}, doneDefault)
assert.NoError(t, err)

return st, vms, minerAddr
}

t.Run("slashing charges gas", func(t *testing.T) {
st, vms, minerAddr := createMinerWithPower(t)
mockSigner, _ := types.NewMockSignersAndKeyInfo(1)

// change worker
msg := types.NewMessage(mockSigner.Addresses[0], minerAddr, 0, types.ZeroAttoFIL, "slashStorageFault", []byte{})

gasPrice, _ := types.NewAttoFILFromFILString(".00001")
gasLimit := types.NewGasUnits(10)
result, err := th.ApplyTestMessageWithGas(st, vms, msg, types.NewBlockHeight(1), &mockSigner, gasPrice, gasLimit, mockSigner.Addresses[0])
require.NoError(t, err)

require.Error(t, result.ExecutionError)
assert.Contains(t, result.ExecutionError.Error(), "Insufficient gas")
assert.Equal(t, uint8(exec.ErrInsufficientGas), result.Receipt.ExitCode)
})

t.Run("slashing a miner with no storage fails", func(t *testing.T) {
ctx := context.Background()
st, vms := th.RequireCreateStorages(ctx, t)
minerAddr := th.CreateTestMiner(t, st, vms, address.TestAddress, th.RequireRandomPeerID(t))

res, err := th.CreateAndApplyTestMessage(t, st, vms, minerAddr, 0, lastPossibleSubmission+1, "slashStorageFault", nil)
require.NoError(t, err)
assert.Contains(t, res.ExecutionError.Error(), "miner is inactive")
assert.Equal(t, uint8(ErrMinerNotSlashable), res.Receipt.ExitCode)
})

t.Run("slashing too early fails", func(t *testing.T) {
st, vms, minerAddr := createMinerWithPower(t)

res, err := th.CreateAndApplyTestMessage(t, st, vms, minerAddr, 0, lastPossibleSubmission, "slashStorageFault", nil)
require.NoError(t, err)
assert.Contains(t, res.ExecutionError.Error(), "miner not yet tardy")
assert.Equal(t, uint8(ErrMinerNotSlashable), res.Receipt.ExitCode)

// assert miner not slashed
assertSlashStatus(t, st, vms, minerAddr, 2*types.OneKiBSectorSize.Uint64(), nil, types.NewIntSet())
})

t.Run("slashing after generation attack time succeeds", func(t *testing.T) {
st, vms, minerAddr := createMinerWithPower(t)

// get storage power prior to fault
oldTotalStoragePower := th.GetTotalPower(t, st, vms)

slashTime := lastPossibleSubmission + 1
res, err := th.CreateAndApplyTestMessage(t, st, vms, minerAddr, 0, slashTime, "slashStorageFault", nil)
require.NoError(t, err)
require.NoError(t, res.ExecutionError)
assert.Equal(t, uint8(0), res.Receipt.ExitCode)

// assert miner has been slashed
assertSlashStatus(t, st, vms, minerAddr, 0, types.NewBlockHeight(slashTime), types.NewIntSet(1, 2))

// assert all miner power (2 small sectors worth) has been removed from totalStoragePower
newTotalStoragePower := th.GetTotalPower(t, st, vms)
assert.Equal(t, types.OneKiBSectorSize.Mul(types.NewBytesAmount(2)), oldTotalStoragePower.Sub(newTotalStoragePower))

// assert proving set and sector set are also updated
minerState := mustGetMinerState(st, vms, minerAddr)
assert.Equal(t, 0, minerState.SectorCommitments.Size(), "slashed sectors are removed from commitments")
assert.Equal(t, 0, minerState.ProvingSet.Size(), "slashed sectors are removed from ProvingSet")

// assert owed collateral is set to active collateral
// TODO: We currently do not know the correct amount of collateral: https://github.com/filecoin-project/go-filecoin/issues/3050
assert.Equal(t, types.ZeroAttoFIL, minerState.OwedStorageCollateral)
})

t.Run("slashing a miner twice fails", func(t *testing.T) {
st, vms, minerAddr := createMinerWithPower(t)

slashTime := lastPossibleSubmission + 1
_, err := th.CreateAndApplyTestMessage(t, st, vms, minerAddr, 0, slashTime, "slashStorageFault", nil)
require.NoError(t, err)

res, err := th.CreateAndApplyTestMessage(t, st, vms, minerAddr, 0, slashTime+1, "slashStorageFault", nil)
require.NoError(t, err)
assert.Contains(t, res.ExecutionError.Error(), "miner already slashed")
assert.Equal(t, uint8(ErrMinerAlreadySlashed), res.Receipt.ExitCode)
})
}

func assertSlashStatus(t *testing.T, st state.Tree, vms vm.StorageMap, minerAddr address.Address, power uint64,
slashedAt *types.BlockHeight, slashed types.IntSet) {
minerState := mustGetMinerState(st, vms, minerAddr)

assert.Equal(t, types.NewBytesAmount(power), minerState.Power)
assert.Equal(t, slashedAt, minerState.SlashedAt)
assert.Equal(t, slashed, minerState.SlashedSet)

}

func TestVerifyPIP(t *testing.T) {
tf.UnitTest(t)

Expand Down Expand Up @@ -1143,6 +1274,21 @@ func mustDeserializeAddress(t *testing.T, result [][]byte) address.Address {
return addr
}

func bh(h uint64) *types.BlockHeight {
return types.NewBlockHeight(uint64(h))
// mustGetMinerState returns the block of actor state represented by the head of the actor with the given address
func mustGetMinerState(st state.Tree, vms vm.StorageMap, a address.Address) *State {
actor := state.MustGetActor(st, a)

storage := vms.NewStorage(a, actor)
data, err := storage.Get(actor.Head)
if err != nil {
panic(err)
}

minerState := &State{}
err = cbor.DecodeInto(data, minerState)
if err != nil {
panic(err)
}

return minerState
}
1 change: 1 addition & 0 deletions actor/builtin/miner_redeem_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ func createStorageMinerWithCommitment(ctx context.Context, st state.Tree, vms vm
SectorCommitments: commitments,
NextDoneSet: types.EmptyIntSet(),
ProvingSet: types.EmptyIntSet(),
SlashedSet: types.EmptyIntSet(),
LastPoSt: lastPoSt,
}
executableActor := miner.Actor{}
Expand Down
7 changes: 3 additions & 4 deletions state/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ package state
import (
"context"
"fmt"
cid "github.com/ipfs/go-cid"

"github.com/stretchr/testify/mock"

"github.com/filecoin-project/go-filecoin/actor"
"github.com/filecoin-project/go-filecoin/address"
"github.com/filecoin-project/go-filecoin/exec"

"github.com/ipfs/go-cid"
"github.com/stretchr/testify/mock"
)

// MustFlush flushes the StateTree or panics if it can't.
Expand Down
25 changes: 10 additions & 15 deletions testhelpers/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,21 +113,6 @@ func VMStorage() vm.StorageMap {
return vm.NewStorageMap(blockstore.NewBlockstore(datastore.NewMapDatastore()))
}

// MustSign signs a given address with the provided mocksigner or panics if it
// cannot.
func MustSign(s types.MockSigner, msgs ...*types.Message) []*types.SignedMessage {
var smsgs []*types.SignedMessage
for _, m := range msgs {
gasLimit := types.NewGasUnits(999)
sm, err := types.NewSignedMessage(*m, &s, types.NewGasPrice(0), gasLimit)
if err != nil {
panic(err)
}
smsgs = append(smsgs, sm)
}
return smsgs
}

// CreateTestMiner creates a new test miner with the given peerID and miner
// owner address within the state tree defined by st and vms with 100 FIL as
// collateral.
Expand Down Expand Up @@ -157,6 +142,16 @@ func CreateTestMinerWith(
return addr
}

// GetTotalPower get total miner power from storage market
func GetTotalPower(t *testing.T, st state.Tree, vms vm.StorageMap) *types.BytesAmount {
res, err := CreateAndApplyTestMessage(t, st, vms, address.StorageMarketAddress, 0, 0, "getTotalStorage", nil)
require.NoError(t, err)
require.NoError(t, res.ExecutionError)
require.Equal(t, uint8(0), res.Receipt.ExitCode)
require.Equal(t, 1, len(res.Receipt.Return))
return types.NewBytesAmountFromBytes(res.Receipt.Return[0])
}

// RequireGetNonce returns the next nonce of the actor at address a within
// state tree st, failing on error.
func RequireGetNonce(t *testing.T, st state.Tree, a address.Address) uint64 {
Expand Down

0 comments on commit 1dee904

Please sign in to comment.