Skip to content

Commit

Permalink
feat(examples): Add a useful set of high quality pseudo-random number…
Browse files Browse the repository at this point in the history
… generators (#2868)

I ported a number of my pseudo-random number generator implementations
from Ruby to gno while traveling to the retreat last weekend as an
exercise in expanding my comfort level with gno code, and expanding my
understanding of some of the code internals, while contributing code
that others may find interesting or useful.

I added two xorshift generators, xorshift64* and xorshiftr128+. These
are both many times faster than the PCG generator that is the gno
default, and produce high quality randomness with great statistical
qualities. In addition to these, I added both the 32-bit ISAAC
implementation (with an added function to return 64 bit values), and the
64-bit ISAAC implementation. ISAAC is a stellar pseudo-random number
generator. Both implementations are significantly faster than PCG
(though not near so fast as the xorshift algorithms), while producing
extremely high quality, cryptographically secure randomness that can not
be differentiated from real randomness.

All of these were built to be compatible with the standard Rand()
implementation. This means that any of these can be used as a drop-in
replacement for the default PCG algorithm:

```
source = isaac.New()
prng := rand.New(source)
```

All of these leverage the `gno.land/p/demo/entropy` package to assist
with seeding if no seed is provided. In the case of the ISAAC
algorithms, they require 256 uint values for their seed, so they
leverage a combination of `entropy` and `xorshiftr128+` to generate any
missing numbers in the provided seed.

I also added a function to entropy to return uint64, to facilitate using
it for seeding.

I added tests to entropy, and wrote tests for the other generators, as
well.

There are a few other things that ended up in this PR. In order to make
some fact based assertions about the performance of these generators, I
included some code that can be ran via `gno run -expr`. i.e. `gno run
-expr 'averageISAAC()' isaac.gno` that can be used to get some
benchmarks and some very simple self-statistical-analysis on the
results, and when I did so, I discovered that the current `ufmt.Sprintf`
implementation didn't support any of the float output flags.

I added float support to it's capabilities, which, in turn, required
adding `FormatFloat` to the `strconv.gno/strconv.go` implementation in
the standard library. I added a test to cover this.

I also noticed that there is a test in `tm2/pkg/p2p` that is failing on
both master and my branch. Specifically, there is a call to
`sw.Logger.Error()` that passes a message and an error, but not `"err"`
before the error. Adding that seemed to clear up the build failure.
This, specifically, is line 222 of `switch.go`.

Currently there is one failing test, which is the code coverage check on
tm2, because it is non-obvious to me how to setup a test to properly
exercise that one changed line.

<details><summary>Contributors' checklist...</summary>

- [X] Added new tests, or not needed, or not feasible
- [X] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [X] Updated the official documentation or not needed
- [X] No breaking changes were made, or a `BREAKING CHANGE: xxx` message
was included in the description
- [X] Added references to related issues and PRs
</details>

---------

Co-authored-by: Morgan <[email protected]>
Co-authored-by: Nathan Toups <[email protected]>
Co-authored-by: Morgan <[email protected]>
  • Loading branch information
4 people authored Dec 5, 2024
1 parent ebb4948 commit b631207
Show file tree
Hide file tree
Showing 20 changed files with 2,218 additions and 0 deletions.
8 changes: 8 additions & 0 deletions examples/gno.land/p/demo/entropy/entropy.gno
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,11 @@ func (i *Instance) Value() uint32 {
i.addEntropy()
return i.value
}

func (i *Instance) Value64() uint64 {
i.addEntropy()
high := i.value
i.addEntropy()

return (uint64(high) << 32) | uint64(i.value)
}
32 changes: 32 additions & 0 deletions examples/gno.land/p/demo/entropy/entropy_test.gno
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,26 @@ func TestInstanceValue(t *testing.T) {
}
}

func TestInstanceValue64(t *testing.T) {
baseEntropy := New()
baseResult := computeValue64(t, baseEntropy)

sameHeightEntropy := New()
sameHeightResult := computeValue64(t, sameHeightEntropy)

if baseResult != sameHeightResult {
t.Errorf("should have the same result: new=%s, base=%s", sameHeightResult, baseResult)
}

std.TestSkipHeights(1)
differentHeightEntropy := New()
differentHeightResult := computeValue64(t, differentHeightEntropy)

if baseResult == differentHeightResult {
t.Errorf("should have different result: new=%s, base=%s", differentHeightResult, baseResult)
}
}

func computeValue(t *testing.T, r *Instance) string {
t.Helper()

Expand All @@ -44,3 +64,15 @@ func computeValue(t *testing.T, r *Instance) string {

return out
}

func computeValue64(t *testing.T, r *Instance) string {
t.Helper()

out := ""
for i := 0; i < 10; i++ {
val := int(r.Value64())
out += strconv.Itoa(val) + " "
}

return out
}
6 changes: 6 additions & 0 deletions examples/gno.land/p/demo/entropy/z_filetest.gno
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ func main() {
println(r.Value())
println(r.Value())
println(r.Value())
println(r.Value64())

// should be the same
println("---")
Expand All @@ -24,6 +25,7 @@ func main() {
println(r.Value())
println(r.Value())
println(r.Value())
println(r.Value64())

std.TestSkipHeights(1)
println("---")
Expand All @@ -33,6 +35,7 @@ func main() {
println(r.Value())
println(r.Value())
println(r.Value())
println(r.Value64())
}

// Output:
Expand All @@ -42,15 +45,18 @@ func main() {
// 1950222777
// 3348280598
// 438354259
// 6353385488959065197
// ---
// 4129293727
// 2141104956
// 1950222777
// 3348280598
// 438354259
// 6353385488959065197
// ---
// 49506731
// 1539580078
// 2695928529
// 1895482388
// 3462727799
// 16745038698684748445
6 changes: 6 additions & 0 deletions examples/gno.land/p/demo/ufmt/ufmt_test.gno
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ func TestSprintf(t *testing.T) {
{"uint32 [%v]", []interface{}{uint32(32)}, "uint32 [32]"},
{"uint64 [%d]", []interface{}{uint64(64)}, "uint64 [64]"},
{"uint64 [%v]", []interface{}{uint64(64)}, "uint64 [64]"},
{"float64 [%e]", []interface{}{float64(64.1)}, "float64 [6.41e+01]"},
{"float64 [%E]", []interface{}{float64(64.1)}, "float64 [6.41E+01]"},
{"float64 [%f]", []interface{}{float64(64.1)}, "float64 [64.100000]"},
{"float64 [%F]", []interface{}{float64(64.1)}, "float64 [64.100000]"},
{"float64 [%g]", []interface{}{float64(64.1)}, "float64 [64.1]"},
{"float64 [%G]", []interface{}{float64(64.1)}, "float64 [64.1]"},
{"bool [%t]", []interface{}{true}, "bool [true]"},
{"bool [%v]", []interface{}{true}, "bool [true]"},
{"bool [%t]", []interface{}{false}, "bool [false]"},
Expand Down
86 changes: 86 additions & 0 deletions examples/gno.land/p/wyhaines/rand/isaac/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# package isaac // import "gno.land/p/demo/math/rand/isaac"

This is a port of the ISAAC cryptographically secure PRNG,
originally based on the reference implementation found at
https://burtleburtle.net/bob/rand/isaacafa.html

ISAAC has excellent statistical properties, with long cycle times, and
uniformly distributed, unbiased, and unpredictable number generation. It can
not be distinguished from real random data, and in three decades of scrutiny,
no practical attacks have been found.

The default random number algorithm in gno was ported from Go's v2 rand
implementatoon, which defaults to the PCG algorithm. This algorithm is
commonly used in language PRNG implementations because it has modest seeding
requirements, and generates statistically strong randomness.

This package provides an implementation of the 32-bit ISAAC PRNG algorithm. This
algorithm provides very strong statistical performance, and is cryptographically
secure, while still being substantially faster than the default PCG
implementation in `math/rand`. Note that this package does implement a `Uint64()`
function in order to generate a 64 bit number out of two 32 bit numbers. Doing this
makes the generator only slightly faster than PCG, however,

Note that the approach to seeing with ISAAC is very important for best results,
and seeding with ISAAC is not as simple as seeding with a single uint64 value.
The ISAAC algorithm requires a 256-element seed. If used for cryptographic
purposes, this will likely require entropy generated off-chain for actual
cryptographically secure seeding. For other purposes, however, one can utilize
the built-in seeding mechanism, which will leverage the xorshiftr128plus PRNG to
generate any missing seeds if fewer than 256 are provided.


```
Benchmark
---------
PCG: 1000000 Uint64 generated in 15.58s
ISAAC: 1000000 Uint64 generated in 13.23s (uint64)
ISAAC: 1000000 Uint32 generated in 6.43s (uint32)
Ratio: x1.18 times faster than PCG (uint64)
Ratio: x2.42 times faster than PCG (uint32)
```

Use it directly:

```
prng = isaac.New() // pass 0 to 256 uint32 seeds; if fewer than 256 are provided, the rest
// will be generated using the xorshiftr128plus PRNG.
```

Or use it as a drop-in replacement for the default PRNT in Rand:

```
source = isaac.New()
prng := rand.New(source)
```

# TYPES

`
type ISAAC struct {
// Has unexported fields.
}
`

`func New(seeds ...uint32) *ISAAC`
ISAAC requires a large, 256-element seed. This implementation will leverage
the entropy package combined with the the xorshiftr128plus PRNG to generate
any missing seeds of fewer than the required number of arguments are
provided.

`func (isaac *ISAAC) MarshalBinary() ([]byte, error)`
MarshalBinary() returns a byte array that encodes the state of the PRNG.
This can later be used with UnmarshalBinary() to restore the state of the
PRNG. MarshalBinary implements the encoding.BinaryMarshaler interface.

`func (isaac *ISAAC) Seed(seed [256]uint32)`

`func (isaac *ISAAC) Uint32() uint32`

`func (isaac *ISAAC) Uint64() uint64`

`func (isaac *ISAAC) UnmarshalBinary(data []byte) error`
UnmarshalBinary() restores the state of the PRNG from a byte array
that was created with MarshalBinary(). UnmarshalBinary implements the
encoding.BinaryUnmarshaler interface.

7 changes: 7 additions & 0 deletions examples/gno.land/p/wyhaines/rand/isaac/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module gno.land/p/wyhaines/rand/isaac

require (
gno.land/p/demo/entropy v0.0.0-latest
gno.land/p/demo/ufmt v0.0.0-latest
gno.land/p/wyhaines/rand/xorshiftr128plus v0.0.0-latest
)
Loading

0 comments on commit b631207

Please sign in to comment.