Skip to content
Open
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
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ performance, cryptographic security, etc., use the
[ulid.Make](https://pkg.go.dev/github.com/oklog/ulid/v2#Make) helper function.
This function calls [time.Now](https://pkg.go.dev/time#Now) to get a timestamp,
and uses a source of entropy which is process-global,
[pseudo-random](https://pkg.go.dev/math/rand), and
[pseudo-random](https://pkg.go.dev/math/rand/v2), and
[monotonic](https://pkg.go.dev/github.com/oklog/ulid/v2#LockedMonotonicReader).

```go
Expand All @@ -69,15 +69,19 @@ More advanced use cases should utilize
[ulid.New](https://pkg.go.dev/github.com/oklog/ulid/v2#New).

```go
entropy := rand.New(rand.NewSource(time.Now().UnixNano()))

seed := [32]byte{}
binary.LittleEndian.PutUint64(seed[:8], uint64(time.Now().UnixNano()))
entropy := rand.NewChaCha8(seed) // This uses math/rand/v2.

ms := ulid.Timestamp(time.Now())
fmt.Println(ulid.New(ms, entropy))
// 01G65Z755AFWAKHE12NY0CQ9FH
```

Care should be taken when providing a source of entropy.

The above example utilizes [math/rand.Rand](https://pkg.go.dev/math/rand#Rand),
The above example utilizes [math/rand/v2.Rand](https://pkg.go.dev/math/rand/v2#Rand),
which is not safe for concurrent use by multiple goroutines. Consider
alternatives such as
[x/exp/rand](https://pkg.go.dev/golang.org/x/exp/rand#LockedSource).
Expand Down
9 changes: 5 additions & 4 deletions cmd/ulid/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ package main

import (
cryptorand "crypto/rand"
"encoding/binary"
"fmt"
mathrand "math/rand"
mathrand "math/rand/v2"
"os"
"strings"
"time"
Expand Down Expand Up @@ -65,9 +66,9 @@ func main() {
func generate(quick, zero bool) {
entropy := cryptorand.Reader
if quick {
seed := time.Now().UnixNano()
source := mathrand.NewSource(seed)
entropy = mathrand.New(source)
seed := [32]byte{}
binary.LittleEndian.PutUint64(seed[:8], uint64(time.Now().UnixNano()))
entropy = mathrand.NewChaCha8(seed)
}
if zero {
entropy = zeroReader{}
Expand Down
6 changes: 4 additions & 2 deletions ulid.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import (
"io"
"math"
"math/bits"
"math/rand"
"math/rand/v2"
"sync"
"time"
)
Expand Down Expand Up @@ -133,7 +133,9 @@ func MustNewDefault(t time.Time) ULID {
}

var defaultEntropy = func() io.Reader {
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
seed := [32]byte{}
binary.LittleEndian.PutUint64(seed[:8], uint64(time.Now().UnixNano()))
rng := rand.NewChaCha8(seed)
return &LockedMonotonicReader{MonotonicReader: Monotonic(rng, 0)}
}()

Expand Down
31 changes: 19 additions & 12 deletions ulid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ package ulid_test
import (
"bytes"
crand "crypto/rand"
"encoding/binary"
"fmt"
"io"
"math"
"math/rand"
"math/rand/v2"
"strings"
"testing"
"testing/iotest"
Expand All @@ -31,9 +32,11 @@ import (

func ExampleULID() {
t := time.Unix(1000000, 0)
entropy := ulid.Monotonic(rand.New(rand.NewSource(t.UnixNano())), 0)
rng := createChaCha8RNG(t)

entropy := ulid.Monotonic(rng, 0)
fmt.Println(ulid.MustNew(ulid.Timestamp(t), entropy))
// Output: 0000XSNJG0MQJHBF4QX1EFD6Y3
// Output: 0000XSNJG02VZD8JHMTREXTNAH
}

func TestNew(t *testing.T) {
Expand Down Expand Up @@ -418,9 +421,8 @@ func TestULIDTime(t *testing.T) {
t.Errorf("got err %v, want %v", got, want)
}

rng := rand.New(rand.NewSource(time.Now().UnixNano()))
for i := 0; i < 1e6; i++ {
ms := uint64(rng.Int63n(int64(maxTime)))
ms := uint64(rand.Int64N(int64(maxTime)))

var id ulid.ULID
if err := id.SetTime(ms); err != nil {
Expand Down Expand Up @@ -587,13 +589,12 @@ func TestScan(t *testing.T) {
}

func TestMonotonic(t *testing.T) {
now := ulid.Now()
for _, e := range []struct {
name string
mk func() io.Reader
}{
{"cryptorand", func() io.Reader { return crand.Reader }},
{"mathrand", func() io.Reader { return rand.New(rand.NewSource(int64(now))) }},
{"mathrand", func() io.Reader { return createChaCha8RNG(time.Now()) }},
} {
for _, inc := range []uint64{
0,
Expand Down Expand Up @@ -655,7 +656,7 @@ func TestMonotonicSafe(t *testing.T) {
t.Parallel()

var (
rng = rand.New(rand.NewSource(time.Now().UnixNano()))
rng = createChaCha8RNG(time.Now())
safe = &ulid.LockedMonotonicReader{MonotonicReader: ulid.Monotonic(rng, 0)}
t0 = ulid.Timestamp(time.Now())
)
Expand Down Expand Up @@ -689,7 +690,7 @@ func TestMonotonicSafe(t *testing.T) {

func TestULID_Bytes(t *testing.T) {
tt := time.Unix(1000000, 0)
entropy := ulid.Monotonic(rand.New(rand.NewSource(tt.UnixNano())), 0)
entropy := ulid.Monotonic(createChaCha8RNG(tt), 0)
id := ulid.MustNew(ulid.Timestamp(tt), entropy)
bid := id.Bytes()
bid[len(bid)-1]++
Expand All @@ -698,6 +699,12 @@ func TestULID_Bytes(t *testing.T) {
}
}

func createChaCha8RNG(t time.Time) *rand.ChaCha8 {
seed := [32]byte{}
binary.LittleEndian.PutUint64(seed[:8], uint64(t.UnixNano()))
return rand.NewChaCha8(seed)
}

func BenchmarkNew(b *testing.B) {
benchmarkMakeULID(b, func(timestamp uint64, entropy io.Reader) {
_, _ = ulid.New(timestamp, entropy)
Expand All @@ -714,7 +721,7 @@ func benchmarkMakeULID(b *testing.B, f func(uint64, io.Reader)) {
b.ReportAllocs()
b.SetBytes(int64(len(ulid.ULID{})))

rng := rand.New(rand.NewSource(time.Now().UnixNano()))
rng := createChaCha8RNG(time.Now())

for _, tc := range []struct {
name string
Expand Down Expand Up @@ -768,7 +775,7 @@ func BenchmarkMustParse(b *testing.B) {
}

func BenchmarkString(b *testing.B) {
entropy := rand.New(rand.NewSource(time.Now().UnixNano()))
entropy := createChaCha8RNG(time.Now())
id := ulid.MustNew(123456, entropy)
b.SetBytes(int64(len(id)))
b.ResetTimer()
Expand All @@ -778,7 +785,7 @@ func BenchmarkString(b *testing.B) {
}

func BenchmarkMarshal(b *testing.B) {
entropy := rand.New(rand.NewSource(time.Now().UnixNano()))
entropy := createChaCha8RNG(time.Now())
buf := make([]byte, ulid.EncodedSize)
id := ulid.MustNew(123456, entropy)

Expand Down