Skip to content
This repository has been archived by the owner on Nov 16, 2022. It is now read-only.

chain/rng: Use HMAC in NIST SP 800-90 for RNG #2349

Merged
merged 4 commits into from
Jul 31, 2020
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
1 change: 1 addition & 0 deletions CHANGELOG_UNRELEASED.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

### Chain (Consensus)

- (chain) [\#2349](https://github.com/bandprotocol/bandchain/pull/2349) chain/rng: Use HMAC in NIST SP 800-90 for RNG.
- (chain) [\#2333](https://github.com/bandprotocol/bandchain/pull/2333) Upgrade to Cosmos-SDK version 0.39.1.

### Chain (Non-consensus)
Expand Down
3 changes: 1 addition & 2 deletions chain/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ require (
github.com/gorilla/mux v1.7.4
github.com/kyokomi/emoji v2.2.4+incompatible
github.com/levigross/grequests v0.0.0-20190908174114-253788527a1a
github.com/onsi/ginkgo v1.8.0 // indirect
github.com/onsi/gomega v1.5.0 // indirect
github.com/oasisprotocol/oasis-core/go v0.0.0-20200730171716-3be2b460b3ac
github.com/peterbourgon/diskv v2.0.1+incompatible
github.com/rakyll/statik v0.1.7 // indirect
github.com/segmentio/kafka-go v0.3.7
Expand Down
291 changes: 287 additions & 4 deletions chain/go.sum

Large diffs are not rendered by default.

35 changes: 21 additions & 14 deletions chain/pkg/bandrng/rng.go
Original file line number Diff line number Diff line change
@@ -1,28 +1,35 @@
package bandrng

import (
"crypto/sha256"
"crypto"
"encoding/binary"

"github.com/oasisprotocol/oasis-core/go/common/crypto/drbg"
)

// Rng implements a simple determinisic random number generator. Starting from an initial seed,
// random uint64 numbers are produced from the first 8 bytes of sha256 results, repeatedly.
// Rng implements a simple determinisic random number generator. Starting from an initial entropy,
// nonce, and personalizationString, it utilizes HMAC_DRBG construct as per NIST Special
// Publication 800-90A to produce a stream of random uint64 integers.
type Rng struct {
seed [sha256.Size]byte
}

func nextSeed(prev [sha256.Size]byte) [sha256.Size]byte {
return sha256.Sum256(prev[:])
rng *drbg.Drbg
}

// NewRng creates a new psudo-random generator, using the given seed as the initial random source.
func NewRng(initSeed string) *Rng {
return &Rng{seed: sha256.Sum256([]byte(initSeed))}
// NewRng creates a new psudo-random generator, using the given seeds as the initial random source.
func NewRng(entropyInput, nonce, personalizationString []byte) (*Rng, error) {
rng, err := drbg.New(crypto.SHA256, entropyInput, nonce, personalizationString)
if err != nil {
return nil, err
}
return &Rng{rng: rng}, nil
}

// NextUint64 returns the next 64-bit unsigned random integer produced by this generator.
func (r *Rng) NextUint64() uint64 {
val := binary.BigEndian.Uint64(r.seed[:8])
r.seed = nextSeed(r.seed)
return val
data := make([]byte, 8)
_, err := r.rng.Read(data)
if err != nil {
// Reaching error is not practically possible in hmbc_drbg's codepath.
panic(err)
}
return binary.BigEndian.Uint64(data)
}
22 changes: 14 additions & 8 deletions chain/pkg/bandrng/rng_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,18 @@ import (
)

func TestRngRandom(t *testing.T) {
r := bandrng.NewRng("SEED")
require.Equal(t, r.NextUint64(), uint64(15735084640102210068))
require.Equal(t, r.NextUint64(), uint64(3485776390957061973))
require.Equal(t, r.NextUint64(), uint64(17609118114147816341))
require.Equal(t, r.NextUint64(), uint64(15960811988050104523))
require.Equal(t, r.NextUint64(), uint64(11919533627209787235))
require.Equal(t, r.NextUint64(), uint64(1371552714025041832))
require.Equal(t, r.NextUint64(), uint64(1582662084421402041))
r, err := bandrng.NewRng([]byte("THIS_IS_A_RANDOM_SEED_LONG_ENOUGH_FOR_ENTROPY"), []byte("1"), []byte("TEST"))
require.NoError(t, err)
require.Equal(t, uint64(5751621621077249396), r.NextUint64())
require.Equal(t, uint64(16474548556352052882), r.NextUint64())
require.Equal(t, uint64(17097048712898369316), r.NextUint64())
require.Equal(t, uint64(10686498023352306525), r.NextUint64())
require.Equal(t, uint64(2144097648649487685), r.NextUint64())
require.Equal(t, uint64(1642256529570429276), r.NextUint64())
require.Equal(t, uint64(1298883664373060799), r.NextUint64())
}

func TestRngRandomNotEnoughEntropy(t *testing.T) {
_, err := bandrng.NewRng([]byte("TOO_SHORT"), []byte("1"), []byte("TEST"))
require.EqualError(t, err, "drbg: insufficient entropyInput")
}
76 changes: 41 additions & 35 deletions chain/pkg/bandrng/sampling_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,29 @@ import (
)

func TestChooseOneOne(t *testing.T) {
r := bandrng.NewRng("SEED")
r, err := bandrng.NewRng([]byte("THIS_IS_A_RANDOM_SEED_LONG_ENOUGH_FOR_ENTROPY"), []byte("1"), []byte("TEST"))
require.NoError(t, err)
weights := []uint64{10, 13, 10, 25, 42} // prefix sum is 10,23,33,58,100

require.Equal(t, bandrng.ChooseOne(r, weights), 4) // rng NextUint64() will return 15735084640102210068
require.Equal(t, bandrng.ChooseOne(r, weights), 4) // rng NextUint64() will return 3485776390957061973
require.Equal(t, bandrng.ChooseOne(r, weights), 3) // rng NextUint64() will return 17609118114147816341
require.Equal(t, bandrng.ChooseOne(r, weights), 2) // rng NextUint64() will return 15960811988050104523
require.Equal(t, bandrng.ChooseOne(r, weights), 3) // rng NextUint64() will return 11919533627209787235
require.Equal(t, 4, bandrng.ChooseOne(r, weights)) // rng NextUint64() will return 5751621621077249396
require.Equal(t, 4, bandrng.ChooseOne(r, weights)) // rng NextUint64() will return 16474548556352052882
require.Equal(t, 1, bandrng.ChooseOne(r, weights)) // rng NextUint64() will return 17097048712898369316
require.Equal(t, 2, bandrng.ChooseOne(r, weights)) // rng NextUint64() will return 10686498023352306525
require.Equal(t, 4, bandrng.ChooseOne(r, weights)) // rng NextUint64() will return 2144097648649487685

r = bandrng.NewRng("SEED")
r, err = bandrng.NewRng([]byte("THIS_IS_A_RANDOM_SEED_LONG_ENOUGH_FOR_ENTROPY"), []byte("1"), []byte("TEST"))
require.NoError(t, err)
weights = []uint64{2, 4, 4} // prefix sum is 2,6,10

require.Equal(t, bandrng.ChooseOne(r, weights), 2) // rng NextUint64() will return 15735084640102210068
require.Equal(t, bandrng.ChooseOne(r, weights), 1) // rng NextUint64() will return 3485776390957061973
require.Equal(t, bandrng.ChooseOne(r, weights), 0) // rng NextUint64() will return 17609118114147816341
require.Equal(t, 2, bandrng.ChooseOne(r, weights)) // rng NextUint64() will return 5751621621077249396
require.Equal(t, 1, bandrng.ChooseOne(r, weights)) // rng NextUint64() will return 16474548556352052882
require.Equal(t, 2, bandrng.ChooseOne(r, weights)) // rng NextUint64() will return 17097048712898369316

}

func TestChooseOnePanic(t *testing.T) {
r := bandrng.NewRng("SEED")
r, err := bandrng.NewRng([]byte("THIS_IS_A_RANDOM_SEED_LONG_ENOUGH_FOR_ENTROPY"), []byte("1"), []byte("TEST"))
require.NoError(t, err)
require.Panics(t, func() {
bandrng.ChooseOne(r, []uint64{math.MaxUint64, math.MaxUint64})
})
Expand All @@ -43,49 +46,52 @@ func TestChooseOnePanic(t *testing.T) {
}

func TestChooseSomeEqualWeights(t *testing.T) {
r := bandrng.NewRng("SEED")
r, err := bandrng.NewRng([]byte("THIS_IS_A_RANDOM_SEED_LONG_ENOUGH_FOR_ENTROPY"), []byte("1"), []byte("TEST"))
require.NoError(t, err)
length := 10
weights := make([]uint64, length)
for idx := 0; idx < length; idx++ {
weights[idx] = 1
}
require.Equal(t, bandrng.ChooseSome(r, weights, 5), []int{8, 0, 6, 7, 4})
require.Equal(t, bandrng.ChooseSome(r, weights, 5), []int{2, 7, 4, 8, 9})
require.Equal(t, bandrng.ChooseSome(r, weights, 5), []int{6, 0, 9, 5, 3})
require.Equal(t, bandrng.ChooseSome(r, weights, 5), []int{2, 7, 0, 3, 9})
require.Equal(t, bandrng.ChooseSome(r, weights, 5), []int{8, 3, 4, 0, 1})
require.Equal(t, bandrng.ChooseSome(r, weights, 5), []int{6, 7, 0, 4, 9})
require.Equal(t, bandrng.ChooseSome(r, weights, 5), []int{0, 4, 5, 2, 1})
require.Equal(t, []int{6, 0, 5, 9, 4}, bandrng.ChooseSome(r, weights, 5))
require.Equal(t, []int{6, 0, 9, 8, 2}, bandrng.ChooseSome(r, weights, 5))
require.Equal(t, []int{5, 6, 1, 8, 3}, bandrng.ChooseSome(r, weights, 5))
require.Equal(t, []int{8, 3, 9, 1, 7}, bandrng.ChooseSome(r, weights, 5))
require.Equal(t, []int{6, 1, 5, 3, 7}, bandrng.ChooseSome(r, weights, 5))
require.Equal(t, []int{4, 9, 3, 1, 5}, bandrng.ChooseSome(r, weights, 5))
require.Equal(t, []int{5, 1, 7, 2, 8}, bandrng.ChooseSome(r, weights, 5))
}

func TestChooseSomeSkewedWeights(t *testing.T) {
r := bandrng.NewRng("SEED")
r, err := bandrng.NewRng([]byte("THIS_IS_A_RANDOM_SEED_LONG_ENOUGH_FOR_ENTROPY"), []byte("1"), []byte("TEST"))
require.NoError(t, err)
length := 10
weights := make([]uint64, length)
for idx := 0; idx < length; idx++ {
weights[idx] = uint64(1 + idx*10)
}
require.Equal(t, bandrng.ChooseSome(r, weights, 5), []int{7, 4, 9, 6, 3})
require.Equal(t, bandrng.ChooseSome(r, weights, 5), []int{6, 9, 5, 8, 3})
require.Equal(t, bandrng.ChooseSome(r, weights, 5), []int{2, 6, 8, 1, 3})
require.Equal(t, bandrng.ChooseSome(r, weights, 5), []int{8, 2, 9, 7, 4})
require.Equal(t, bandrng.ChooseSome(r, weights, 5), []int{8, 9, 7, 6, 4})
require.Equal(t, bandrng.ChooseSome(r, weights, 5), []int{7, 5, 8, 9, 6})
require.Equal(t, bandrng.ChooseSome(r, weights, 5), []int{5, 8, 6, 7, 9})
require.Equal(t, []int{8, 7, 2, 9, 6}, bandrng.ChooseSome(r, weights, 5))
require.Equal(t, []int{6, 8, 7, 4, 3}, bandrng.ChooseSome(r, weights, 5))
require.Equal(t, []int{7, 4, 8, 6, 5}, bandrng.ChooseSome(r, weights, 5))
require.Equal(t, []int{8, 5, 4, 9, 1}, bandrng.ChooseSome(r, weights, 5))
require.Equal(t, []int{6, 1, 7, 9, 8}, bandrng.ChooseSome(r, weights, 5))
require.Equal(t, []int{9, 7, 5, 4, 2}, bandrng.ChooseSome(r, weights, 5))
require.Equal(t, []int{3, 9, 5, 8, 7}, bandrng.ChooseSome(r, weights, 5))
}

func TestChooseSomeMaxWeight(t *testing.T) {
r := bandrng.NewRng("SEED")
r, err := bandrng.NewRng([]byte("THIS_IS_A_RANDOM_SEED_LONG_ENOUGH_FOR_ENTROPY"), []byte("1"), []byte("TEST"))
require.NoError(t, err)
length := 10
weights := make([]uint64, length)
for idx := 0; idx < length; idx++ {
weights[idx] = uint64(1 + idx*10)
}
require.Equal(t, bandrng.ChooseSomeMaxWeight(r, weights, 5, 3), []int{6, 9, 5, 8, 3})
require.Equal(t, bandrng.ChooseSomeMaxWeight(r, weights, 5, 3), []int{7, 5, 8, 9, 6})
require.Equal(t, bandrng.ChooseSomeMaxWeight(r, weights, 5, 3), []int{5, 8, 6, 7, 9})
require.Equal(t, bandrng.ChooseSomeMaxWeight(r, weights, 5, 3), []int{6, 8, 9, 7, 4})
require.Equal(t, bandrng.ChooseSomeMaxWeight(r, weights, 5, 3), []int{7, 9, 3, 8, 2})
require.Equal(t, bandrng.ChooseSomeMaxWeight(r, weights, 5, 3), []int{1, 7, 8, 9, 5})
require.Equal(t, bandrng.ChooseSomeMaxWeight(r, weights, 5, 3), []int{9, 8, 6, 5, 3})
require.Equal(t, []int{8, 7, 2, 9, 6}, bandrng.ChooseSomeMaxWeight(r, weights, 5, 3))
require.Equal(t, []int{6, 1, 7, 9, 8}, bandrng.ChooseSomeMaxWeight(r, weights, 5, 3))
require.Equal(t, []int{9, 3, 8, 7, 6}, bandrng.ChooseSomeMaxWeight(r, weights, 5, 3))
require.Equal(t, []int{9, 8, 4, 6, 5}, bandrng.ChooseSomeMaxWeight(r, weights, 5, 3))
require.Equal(t, []int{8, 9, 6, 7, 5}, bandrng.ChooseSomeMaxWeight(r, weights, 5, 3))
require.Equal(t, []int{3, 9, 7, 6, 8}, bandrng.ChooseSomeMaxWeight(r, weights, 5, 3))
require.Equal(t, []int{4, 6, 9, 5, 7}, bandrng.ChooseSomeMaxWeight(r, weights, 5, 3))
}
5 changes: 4 additions & 1 deletion chain/x/oracle/keeper/owasm.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ func (k Keeper) GetRandomValidators(ctx sdk.Context, size int, id int64) ([]sdk.
return nil, sdkerrors.Wrapf(
types.ErrInsufficientValidators, "%d < %d", len(valOperators), size)
}
rng := bandrng.NewRng(fmt.Sprintf("%x:%d", k.GetRollingSeed(ctx), id))
rng, err := bandrng.NewRng(k.GetRollingSeed(ctx), sdk.Uint64ToBigEndian(uint64(id)), []byte(ctx.ChainID()))
if err != nil {
return nil, sdkerrors.Wrapf(types.ErrBadDrbgInitialization, err.Error())
}
tryCount := int(k.GetParam(ctx, types.KeySamplingTryCount))
chosenValIndexes := bandrng.ChooseSomeMaxWeight(rng, valPowers, size, tryCount)
validators := make([]sdk.ValAddress, size)
Expand Down
26 changes: 13 additions & 13 deletions chain/x/oracle/keeper/owasm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,26 @@ import (

func TestGetRandomValidatorsSuccessActivateAll(t *testing.T) {
_, ctx, k := testapp.CreateTestInput(true)
// Getting 3 validators using ROLLING_SEED_1
k.SetRollingSeed(ctx, []byte("ROLLING_SEED_1"))
// Getting 3 validators using ROLLING_SEED_1_WITH_LONG_ENOUGH_ENTROPY
k.SetRollingSeed(ctx, []byte("ROLLING_SEED_1_WITH_LONG_ENOUGH_ENTROPY"))
vals, err := k.GetRandomValidators(ctx, 3, 1)
require.NoError(t, err)
require.Equal(t, []sdk.ValAddress{testapp.Validator1.ValAddress, testapp.Validator3.ValAddress, testapp.Validator2.ValAddress}, vals)
// Getting 3 validators using ROLLING_SEED_A
k.SetRollingSeed(ctx, []byte("ROLLING_SEED_A"))
vals, err = k.GetRandomValidators(ctx, 3, 1)
require.NoError(t, err)
require.Equal(t, []sdk.ValAddress{testapp.Validator3.ValAddress, testapp.Validator1.ValAddress, testapp.Validator2.ValAddress}, vals)
// Getting 3 validators using ROLLING_SEED_1 again should return the same result as the first one.
k.SetRollingSeed(ctx, []byte("ROLLING_SEED_1"))
// Getting 3 validators using ROLLING_SEED_A
k.SetRollingSeed(ctx, []byte("ROLLING_SEED_A_WITH_LONG_ENOUGH_ENTROPY"))
vals, err = k.GetRandomValidators(ctx, 3, 1)
require.NoError(t, err)
require.Equal(t, []sdk.ValAddress{testapp.Validator1.ValAddress, testapp.Validator3.ValAddress, testapp.Validator2.ValAddress}, vals)
// Getting 3 validators using ROLLING_SEED_1 but for a different request ID.
k.SetRollingSeed(ctx, []byte("ROLLING_SEED_1"))
vals, err = k.GetRandomValidators(ctx, 3, 2)
// Getting 3 validators using ROLLING_SEED_1_WITH_LONG_ENOUGH_ENTROPY again should return the same result as the first one.
k.SetRollingSeed(ctx, []byte("ROLLING_SEED_1_WITH_LONG_ENOUGH_ENTROPY"))
vals, err = k.GetRandomValidators(ctx, 3, 1)
require.NoError(t, err)
require.Equal(t, []sdk.ValAddress{testapp.Validator3.ValAddress, testapp.Validator1.ValAddress, testapp.Validator2.ValAddress}, vals)
// Getting 3 validators using ROLLING_SEED_1_WITH_LONG_ENOUGH_ENTROPY but for a different request ID.
k.SetRollingSeed(ctx, []byte("ROLLING_SEED_1_WITH_LONG_ENOUGH_ENTROPY"))
vals, err = k.GetRandomValidators(ctx, 3, 42)
require.NoError(t, err)
require.Equal(t, []sdk.ValAddress{testapp.Validator1.ValAddress, testapp.Validator3.ValAddress, testapp.Validator2.ValAddress}, vals)
}

func TestGetRandomValidatorsTooBigSize(t *testing.T) {
Expand All @@ -53,7 +53,7 @@ func TestGetRandomValidatorsTooBigSize(t *testing.T) {

func TestGetRandomValidatorsWithActivate(t *testing.T) {
_, ctx, k := testapp.CreateTestInput(false)
k.SetRollingSeed(ctx, []byte("ROLLING_SEED"))
k.SetRollingSeed(ctx, []byte("ROLLING_SEED_WITH_LONG_ENOUGH_ENTROPY"))
// If no validators are active, you must not be able to get random validators
_, err := k.GetRandomValidators(ctx, 1, 1)
require.Error(t, err)
Expand Down
1 change: 1 addition & 0 deletions chain/x/oracle/types/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ var (
ErrOBIDecode = sdkerrors.Register(ModuleName, 37, "obi decode failed")
ErrUncompressionFailed = sdkerrors.Register(ModuleName, 38, "uncompression failed")
ErrRequestAlreadyExpired = sdkerrors.Register(ModuleName, 39, "request already expired")
ErrBadDrbgInitialization = sdkerrors.Register(ModuleName, 40, "bad drbg initialization")
)

// WrapMaxError wraps an error message with additional info of the current and max values.
Expand Down