From 0edc3e9c95f198cc5a20c3f94badb6d86160d7cc Mon Sep 17 00:00:00 2001 From: anorth Date: Mon, 8 Jul 2019 11:33:21 +1000 Subject: [PATCH] Chain sampling for challenge seed skips over null blocks. Fixes #3017. Removed tests for VM context that are duplicates of tests for the actual chain sampling. --- actor/builtin/miner/miner.go | 12 ----- chain/get_ancestors.go | 2 + sampling/chain_randomness.go | 59 ++++++++++------------ sampling/chain_randomness_test.go | 78 ++++++++++++++++------------ testhelpers/mining.go | 10 ++-- vm/context_test.go | 84 ------------------------------- 6 files changed, 79 insertions(+), 166 deletions(-) diff --git a/actor/builtin/miner/miner.go b/actor/builtin/miner/miner.go index 80c89c97cc..af020527c7 100644 --- a/actor/builtin/miner/miner.go +++ b/actor/builtin/miner/miner.go @@ -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) { diff --git a/chain/get_ancestors.go b/chain/get_ancestors.go index 73b351d3f5..8718e0a767 100644 --- a/chain/get_ancestors.go +++ b/chain/get_ancestors.go @@ -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) diff --git a/sampling/chain_randomness.go b/sampling/chain_randomness.go index db0a6d3023..e71e63294c 100644 --- a/sampling/chain_randomness.go +++ b/sampling/chain_randomness.go @@ -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. 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) { 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() } diff --git a/sampling/chain_randomness_test.go b/sampling/chain_randomness_test.go index bac352bbf7..97aa8cdf49 100644 --- a/sampling/chain_randomness_test.go +++ b/sampling/chain_randomness_test.go @@ -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) @@ -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) }) } diff --git a/testhelpers/mining.go b/testhelpers/mining.go index 75c06646ce..2faa17cd6e 100644 --- a/testhelpers/mining.go +++ b/testhelpers/mining.go @@ -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 } diff --git a/vm/context_test.go b/vm/context_test.go index 25775163c2..fe00085d5b 100644 --- a/vm/context_test.go +++ b/vm/context_test.go @@ -2,7 +2,6 @@ package vm import ( "context" - "strconv" "testing" "github.com/ipfs/go-cid" @@ -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" @@ -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) - }) -}