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

Chain sampling for challenge seed skips over null blocks. #3026

Merged
merged 1 commit into from
Jul 9, 2019
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
12 changes: 0 additions & 12 deletions actor/builtin/miner/miner.go
Original file line number Diff line number Diff line change
Expand Up @@ -990,18 +990,6 @@ func LatePoStFee(pledgeCollateral types.AttoFIL, provingPeriodEnd *types.BlockHe
// Internal functions
//

func currentProvingPeriodPoStChallengeSeed(ctx exec.VMContext, state State) (types.PoStChallengeSeed, error) {
bytes, err := ctx.SampleChainRandomness(state.ProvingPeriodEnd)
if err != nil {
return types.PoStChallengeSeed{}, err
}

seed := types.PoStChallengeSeed{}
copy(seed[:], bytes)

return seed, nil
}

// TODO: This is a fake implementation pending availability of the verification algorithm in rust proofs
// see https://github.com/filecoin-project/go-filecoin/issues/2629
func verifyInclusionProof(commP types.CommP, commD types.CommD, proof []byte) (bool, error) {
Expand Down
2 changes: 2 additions & 0 deletions chain/get_ancestors.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ func GetRecentAncestorsOfHeaviestChain(ctx context.Context, chainReader recentAn
// the length of provingPeriodAncestors may vary (more null blocks -> shorter length). The
// length of slice extraRandomnessAncestors is a constant (at least once the
// chain is longer than lookback tipsets).
// This is all more complex than necessary, we should just index tipsets by height:
// https://github.com/filecoin-project/go-filecoin/issues/3025
func GetRecentAncestors(ctx context.Context, base types.TipSet, chainReader recentAncestorsChainReader, childBH, ancestorRoundsNeeded *types.BlockHeight, lookback uint) (ts []types.TipSet, err error) {
ctx, span := trace.StartSpan(ctx, "Chain.GetRecentAncestors")
defer tracing.AddErrorEndSpan(ctx, span, &err)
Expand Down
59 changes: 27 additions & 32 deletions sampling/chain_randomness.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,62 +6,57 @@ import (
"github.com/filecoin-project/go-filecoin/types"
)

// LookbackParameter is the protocol parameter defining how many blocks in the
// past to look back to sample randomness values.
// LookbackParameter defines how many non-empty tiptsets (not rounds) earlier than any sample
// height (in rounds) from which to sample the chain for randomness.
// This constant is a protocol (actor) parameter and should be defined in actor code.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙏

const LookbackParameter = 3

// SampleChainRandomness produces a slice of random bytes sampled from a tip set
// in the provided slice of tip sets at a given height (minus lookback). This
// function assumes that the tip set slice is sorted by block height in
// descending order.
//
// SampleChainRandomness is useful for things like PoSt challenge seed
// generation.
func SampleChainRandomness(sampleHeight *types.BlockHeight, tipSetsSortedByBlockHeightDescending []types.TipSet) ([]byte, error) {
// SampleChainRandomness produces a slice of bytes (a ticket) sampled from the tipset `LookbackParameter`
// tipsets (not rounds) prior to the highest tipset with height less than or equal to `sampleHeight`.
// The tipset slice must be sorted by descending block height.
func SampleChainRandomness(sampleHeight *types.BlockHeight, tipSetsDescending []types.TipSet) ([]byte, error) {
// Find the first (highest) tipset with height less than or equal to sampleHeight.
// This is more complex than necessary: https://github.com/filecoin-project/go-filecoin/issues/3025
sampleIndex := -1
tipSetsLen := len(tipSetsSortedByBlockHeightDescending)
lastIdxInTipSets := tipSetsLen - 1

for i := 0; i < tipSetsLen; i++ {
height, err := tipSetsSortedByBlockHeightDescending[i].Height()
for i, tip := range tipSetsDescending {
height, err := tip.Height()
if err != nil {
return nil, errors.Wrap(err, "error obtaining tip set height")
}

if types.NewBlockHeight(height).Equal(sampleHeight) {
if types.NewBlockHeight(height).LessEqual(sampleHeight) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should do this. While it will probably work for some time this is not compatible with a finished PoSt generate/verify system. We need to sample an exact seed from an exact block height so that proving and verifying interoperate correctly and we should be able to provide it every time. I believe the root of this bug is that the miner actor is getting into a state where its provingPeriodStart is not 100% for sure associated with a tipset blockheight that exists. We should address this directly at the root.

I'm looking into this. cc @acruikshank I can't see any issue with the change here but it is my primary suspect.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking closer at the commit mentioned above I see that we do have a consistent reference seed. In light of that this change is ok, especially since all seed sampling code is going to be seeing some major improvements in the near future.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with the second comment. This will generate a consistent seed and is the correct approach.

sampleIndex = i
break
}
}

// Produce an error if no tip set exists in `tipSetsSortedByBlockHeightDescending` with
// block height `sampleHeight`.
// Produce an error if the slice does not include any tipsets at least as low as `sampleHeight`.
if sampleIndex == -1 {
return nil, errors.Errorf("sample height out of range: %s", sampleHeight)
}

// If looking backwards in time `Lookback`-number of tip sets from the tip
// set with `sampleHeight` would put us farther back in time than the lowest
// height tip set in the slice, then check to see if the lowest height tip
// set is the genesis block. If it is, use its randomness. If not, produce
// an error.
//
// TODO: security, spec, bootstrap implications.
// See issue https://github.com/filecoin-project/go-filecoin/issues/1872
// Now look LookbackParameter tipsets (not rounds) prior to the sample tipset.
lookbackIdx := sampleIndex + LookbackParameter
if lookbackIdx > lastIdxInTipSets {
leastHeightInChain, err := tipSetsSortedByBlockHeightDescending[lastIdxInTipSets].Height()
lastIdx := len(tipSetsDescending) - 1
if lookbackIdx > lastIdx {
// If this index farther than the lowest height (last) tipset in the slice, then
// - if the lowest is the genesis, use that, else
// - error (the tipset slice is insufficient)
//
// TODO: security, spec, bootstrap implications.
// See issue https://github.com/filecoin-project/go-filecoin/issues/1872
lowestAvailableHeight, err := tipSetsDescending[lastIdx].Height()
if err != nil {
return nil, errors.Wrap(err, "error obtaining tip set height")
}

if leastHeightInChain == uint64(0) {
lookbackIdx = lastIdxInTipSets
if lowestAvailableHeight == uint64(0) {
lookbackIdx = lastIdx
} else {
errMsg := "sample height out of range: lookbackIdx=%d, lastHeightInChain=%d"
return nil, errors.Errorf(errMsg, lookbackIdx, leastHeightInChain)
return nil, errors.Errorf(errMsg, lookbackIdx, lowestAvailableHeight)
}
}

return tipSetsSortedByBlockHeightDescending[lookbackIdx].MinTicket()
return tipSetsDescending[lookbackIdx].MinTicket()
}
78 changes: 45 additions & 33 deletions sampling/chain_randomness_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func TestSamplingChainRandomness(t *testing.T) {
require.Equal(t, sampling.LookbackParameter, 3, "these tests assume LookbackParameter=3")

t.Run("happy path", func(t *testing.T) {

// The tipsets are in descending height order. Each block's ticket is its stringified height (as bytes).
chain := testhelpers.RequireTipSetChain(t, 20)

r, err := sampling.SampleChainRandomness(types.NewBlockHeight(uint64(20)), chain)
Expand All @@ -36,52 +36,64 @@ func TestSamplingChainRandomness(t *testing.T) {
assert.Equal(t, []byte(strconv.Itoa(7)), r)
})

t.Run("faults with height out of range", func(t *testing.T) {

t.Run("skips missing tipsets", func(t *testing.T) {
chain := testhelpers.RequireTipSetChain(t, 20)

// edit chain to include null blocks at heights 21 through 24
baseBlock := chain[1].ToSlice()[0]
afterNull := types.NewBlockForTest(baseBlock, uint64(0))
afterNull.Height += types.Uint64(uint64(5))
afterNull.Ticket = []byte(strconv.Itoa(int(afterNull.Height)))
chain = append([]types.TipSet{types.RequireNewTipSet(t, afterNull)}, chain...)

// ancestor block heights:
//
// 25 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
//
// no tip set with height 30 exists in ancestors
_, err := sampling.SampleChainRandomness(types.NewBlockHeight(uint64(30)), chain)
assert.Error(t, err)
})
// Sample height after the head falls back to the head, and then looks back from there
r, err := sampling.SampleChainRandomness(types.NewBlockHeight(uint64(25)), chain)
assert.NoError(t, err)
assert.Equal(t, []byte(strconv.Itoa(17)), r)

t.Run("faults with lookback out of range", func(t *testing.T) {
// Add new head so as to produce null blocks between 20 and 25
// i.e.: 25 20 19 18 ... 0
headAfterNulls := types.NewBlockForTest(chain[0].ToSlice()[0], uint64(0))
headAfterNulls.Height = types.Uint64(uint64(25))
headAfterNulls.Ticket = []byte(strconv.Itoa(int(headAfterNulls.Height)))
chain = append([]types.TipSet{types.RequireNewTipSet(t, headAfterNulls)}, chain...)

// Sampling in the nulls falls back to the last non-null
r, err = sampling.SampleChainRandomness(types.NewBlockHeight(uint64(24)), chain)
assert.NoError(t, err)
assert.Equal(t, []byte(strconv.Itoa(17)), r)

// When sampling immediately after the nulls, the look-back skips the nulls (not counting them).
r, err = sampling.SampleChainRandomness(types.NewBlockHeight(uint64(25)), chain)
assert.NoError(t, err)
assert.Equal(t, []byte(strconv.Itoa(18)), r)
})

t.Run("fails when chain insufficient", func(t *testing.T) {
// Chain: 20, 19, 18, 17, 16
// The final tipset is not of height zero (genesis)
chain := testhelpers.RequireTipSetChain(t, 20)[:5]

// ancestor block heights:
//
// 20, 19, 18, 17, 16
//
// going back in time by `LookbackParameter`-number of tip sets from
// block height 17 does not find us the genesis block
_, err := sampling.SampleChainRandomness(types.NewBlockHeight(uint64(17)), chain)
// Sample is out of range
_, err := sampling.SampleChainRandomness(types.NewBlockHeight(uint64(15)), chain)
assert.Error(t, err)

// Sample minus lookback is out of range
_, err = sampling.SampleChainRandomness(types.NewBlockHeight(uint64(16)), chain)
assert.Error(t, err)
_, err = sampling.SampleChainRandomness(types.NewBlockHeight(uint64(18)), chain)
assert.Error(t, err)

// Ok when the chain is just sufficiently long.
r, err := sampling.SampleChainRandomness(types.NewBlockHeight(uint64(19)), chain)
assert.NoError(t, err)
assert.Equal(t, []byte(strconv.Itoa(16)), r)
})

t.Run("falls back to genesis block", func(t *testing.T) {

chain := testhelpers.RequireTipSetChain(t, 5)

// ancestor block heights:
//
// 5, 3, 2, 1, 0
//
// going back in time by `LookbackParameter`-number of tip sets from 1
// would put us into the negative - so fall back to genesis block
// Three blocks back from "1"
r, err := sampling.SampleChainRandomness(types.NewBlockHeight(uint64(1)), chain)
assert.NoError(t, err)
assert.Equal(t, []byte(strconv.Itoa(0)), r)

// Sample height can be zero.
r, err = sampling.SampleChainRandomness(types.NewBlockHeight(uint64(0)), chain)
assert.NoError(t, err)
assert.Equal(t, []byte(strconv.Itoa(0)), r)
})
}
10 changes: 5 additions & 5 deletions testhelpers/mining.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,21 +52,21 @@ func MakeRandomBytes(size int) []byte {
return comm
}

// RequireTipSetChain produces a chain of TipSet of the requested length. The
// TipSet with greatest height will be at the front of the returned slice.
func RequireTipSetChain(t *testing.T, numTipSets int) []types.TipSet {
// RequireTipSetChain produces a chain of singleton tipsets up to the requested height (i.e. of
// length height+1). The tipset with greatest height will be at the front of the returned slice.
func RequireTipSetChain(t *testing.T, toHeight int) []types.TipSet {
var tipSetsDescBlockHeight []types.TipSet
// setup ancestor chain
head := types.NewBlockForTest(nil, uint64(0))
head.Ticket = []byte(strconv.Itoa(0))
for i := 0; i < numTipSets; i++ {
for i := 0; i < toHeight; i++ {
tipSetsDescBlockHeight = append([]types.TipSet{types.RequireNewTipSet(t, head)}, tipSetsDescBlockHeight...)
newBlock := types.NewBlockForTest(head, uint64(0))
newBlock.Ticket = []byte(strconv.Itoa(i + 1))
head = newBlock
}

// The final tipset has height `toHeight`.
tipSetsDescBlockHeight = append([]types.TipSet{types.RequireNewTipSet(t, head)}, tipSetsDescBlockHeight...)

return tipSetsDescBlockHeight
}
84 changes: 0 additions & 84 deletions vm/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package vm

import (
"context"
"strconv"
"testing"

"github.com/ipfs/go-cid"
Expand All @@ -20,7 +19,6 @@ import (
"github.com/filecoin-project/go-filecoin/actor/builtin/account"
"github.com/filecoin-project/go-filecoin/address"
"github.com/filecoin-project/go-filecoin/exec"
"github.com/filecoin-project/go-filecoin/sampling"
"github.com/filecoin-project/go-filecoin/state"
tf "github.com/filecoin-project/go-filecoin/testhelpers/testflags"
"github.com/filecoin-project/go-filecoin/types"
Expand Down Expand Up @@ -285,85 +283,3 @@ func TestVMContextIsAccountActor(t *testing.T) {
ctx = NewVMContext(vmCtxParams)
assert.False(t, ctx.IsFromAccountActor())
}

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

var tipSetsDescBlockHeight []types.TipSet
// setup ancestor chain
head := types.NewBlockForTest(nil, uint64(0))
head.Ticket = []byte(strconv.Itoa(0))
for i := 0; i < 20; i++ {
tipSetsDescBlockHeight = append([]types.TipSet{types.RequireNewTipSet(t, head)}, tipSetsDescBlockHeight...)
newBlock := types.NewBlockForTest(head, uint64(0))
newBlock.Ticket = []byte(strconv.Itoa(i + 1))
head = newBlock
}
tipSetsDescBlockHeight = append([]types.TipSet{types.RequireNewTipSet(t, head)}, tipSetsDescBlockHeight...)

// set a tripwire
require.Equal(t, sampling.LookbackParameter, 3, "these tests assume LookbackParameter=3")

t.Run("happy path", func(t *testing.T) {
ctx := NewVMContext(NewContextParams{
Ancestors: tipSetsDescBlockHeight,
})

r, err := ctx.SampleChainRandomness(types.NewBlockHeight(uint64(20)))
assert.NoError(t, err)
assert.Equal(t, []byte(strconv.Itoa(17)), r)

r, err = ctx.SampleChainRandomness(types.NewBlockHeight(uint64(3)))
assert.NoError(t, err)
assert.Equal(t, []byte(strconv.Itoa(0)), r)

r, err = ctx.SampleChainRandomness(types.NewBlockHeight(uint64(10)))
assert.NoError(t, err)
assert.Equal(t, []byte(strconv.Itoa(7)), r)
})

t.Run("faults with height out of range", func(t *testing.T) {
// edit `tipSetsDescBlockHeight` to include null blocks at heights 21
// through 24
baseBlock := tipSetsDescBlockHeight[1].ToSlice()[0]
afterNull := types.NewBlockForTest(baseBlock, uint64(0))
afterNull.Height += types.Uint64(uint64(5))
afterNull.Ticket = []byte(strconv.Itoa(int(afterNull.Height)))
modAncestors := append([]types.TipSet{types.RequireNewTipSet(t, afterNull)}, tipSetsDescBlockHeight...)

// ancestor block heights:
//
// 25 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
ctx := NewVMContext(NewContextParams{
Ancestors: modAncestors,
})

// no tip set with height 30 exists in ancestors
_, err := ctx.SampleChainRandomness(types.NewBlockHeight(uint64(30)))
assert.Error(t, err)
})

t.Run("faults with lookback out of range", func(t *testing.T) {
// ancestor block heights:
//
// 25 20
ctx := NewVMContext(NewContextParams{
Ancestors: tipSetsDescBlockHeight[:5],
})

// going back in time by `LookbackParameter`-number of tip sets from
// block height 25 does not find us the genesis block
_, err := ctx.SampleChainRandomness(types.NewBlockHeight(uint64(25)))
assert.Error(t, err)
})

t.Run("falls back to genesis block", func(t *testing.T) {
vmCtxParams := NewContextParams{
Ancestors: tipSetsDescBlockHeight,
}
ctx := NewVMContext(vmCtxParams)
r, err := ctx.SampleChainRandomness(types.NewBlockHeight(uint64(1))) // lookback height lower than all tipSetsDescBlockHeight
assert.NoError(t, err)
assert.Equal(t, []byte(strconv.Itoa(0)), r)
})
}