Skip to content

Commit

Permalink
Chain sampling for challenge seed skips over null blocks.
Browse files Browse the repository at this point in the history
Fixes #3017.

Removed tests for VM context that are duplicates of tests for the actual chain sampling.
  • Loading branch information
anorth authored and ZenGround0 committed Jul 8, 2019
1 parent 81e75fd commit 0edc3e9
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 166 deletions.
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.
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()
}
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)
})
}

0 comments on commit 0edc3e9

Please sign in to comment.