From 34b2425706074ef0774fc7c4cdb8eb2bd7427e15 Mon Sep 17 00:00:00 2001 From: gammazero <11790789+gammazero@users.noreply.github.com> Date: Wed, 8 Jul 2026 10:21:22 -1000 Subject: [PATCH 1/9] feat: allow reuse of random source For tests that need to keep generating random test data, it is more efficient to continue using the same random source instead of creating a new one each time data is generated. This also applies to the generation logic itself when one part of the generation logic uses another. This PR lets the caller create a pseudo-random number source that can be uses reused for generating test data. This is also use within the test logic itself to avoid creating unnecessary random sources. Also included in this PR are two new functions, `Name` and `NameSize`, that generate fixed-size and random-size random name strings. Finally this PR fixes two minor defects in the `random/files` package. First, random-size names never reached the maximum size. Second, the file names were more likely to contain a "0" due to its inclusion twice in the alphabet used for file name generation. In summary, this PR: - Enables reuse of pseudo-random source for data generation - Makes some data generation logic more efficient by reuse of random source - Fixes minor defects with random file name generation - Increases test coverage. - Adds two new APIs `Name` and `NameSize`, for generating fixes and random-sized random names. --- random/files/files.go | 20 ++- random/random.go | 389 ++++++++++++++++++++++++++++++------------ random/random_test.go | 142 ++++++++++++++- 3 files changed, 433 insertions(+), 118 deletions(-) diff --git a/random/files/files.go b/random/files/files.go index e2fc564..81e3c48 100644 --- a/random/files/files.go +++ b/random/files/files.go @@ -12,8 +12,8 @@ import ( ) const ( - MinimumNameSize = 4 - fileNameAlpha = "abcdefghijklmnopqrstuvwxyz01234567890-_" + MinimumNameSize = 4 + fileNameAlphabet = "abcdefghijklmnopqrstuvwxyz0123456789-_" ) // Config contains settings for creating random files and directories. @@ -218,15 +218,19 @@ func (cfg *Config) writeSubdir(rnd *rand.Rand, root string, depth int) error { func (cfg *Config) randomName(rnd *rand.Rand) string { sizeDiff := cfg.NameMaxSize - cfg.NameMinSize - n := cfg.NameMinSize + size := cfg.NameMinSize if sizeDiff != 0 { - n += rnd.Intn(sizeDiff) + size += rnd.Intn(sizeDiff + 1) } - b := make([]byte, n) - for i := 0; i < n; i++ { - b[i] = fileNameAlpha[rnd.Intn(len(fileNameAlpha))] + if size <= 0 { + return "" } - return string(b) + src := make([]byte, size) + rnd.Read(src) + for i := range src { + src[i] = fileNameAlphabet[src[i]%byte(len(fileNameAlphabet))] + } + return string(src) } func (cfg *Config) writeFile(rnd *rand.Rand, root string) error { diff --git a/random/random.go b/random/random.go index 2e95633..8dcb34d 100644 --- a/random/random.go +++ b/random/random.go @@ -30,18 +30,6 @@ func init() { SetSeed(time.Now().UTC().UnixNano()) } -// NewRand returns a new pseudo-random number source, seeded with the next -// value of a global sequence. -func NewRand() *rand.Rand { - return NewSeededRand(globalSeed.Add(1)) -} - -// NewSeededRand returns a new pseudo-random number source seeded with the -// specified value. -func NewSeededRand(seed int64) *rand.Rand { - return rand.New(rand.NewSource(seed)) -} - // Returns the initial seed used for the pseudo-random number generator, or the // most recent value set by SetSeed. func Seed() int64 { @@ -58,13 +46,33 @@ func SetSeed(seed int64) { globalSeqGen.Store(rng.Uint64()) } +// Random contains its own pseudo-random source. The same Random instance must +// not be used concurrently. Create separate Random instances for use in +// concurrent goroutines. +type Random struct { + *rand.Rand +} + +// New returns a new Random instance that contains its own pseudo-random +// number source, seeded with the next value of a global sequence. +func New() *Random { + return NewSeeded(globalSeed.Add(1)) +} + +// NewSeeded returns a new Random instance that contains its own pseudo-random +// number source seeded with the specified value. +func NewSeeded(seed int64) *Random { + return &Random{ + Rand: rand.New(rand.NewSource(seed)), + } +} + // Addrs returns a slice of n random unique IPv4 addresses. -func Addrs(n int) []string { +func (r *Random) Addrs(n int) []string { addrs := make([]string, n) addrSet := make(map[string]struct{}, n) - rng := NewRand() for i := 0; i < n; i++ { - addr := fmt.Sprintf("/ip4/%d.%d.%d.%d/tcp/%d", rng.Intn(254)+1, rng.Intn(254)+1, rng.Intn(254)+1, rng.Intn(254)+1, rng.Intn(maxTcpPort-minTcpPort)+minTcpPort) + addr := fmt.Sprintf("/ip4/%d.%d.%d.%d/tcp/%d", r.Intn(254)+1, r.Intn(254)+1, r.Intn(254)+1, r.Intn(254)+1, r.Intn(maxTcpPort-minTcpPort)+minTcpPort) if _, ok := addrSet[addr]; ok { i-- continue @@ -76,20 +84,12 @@ func Addrs(n int) []string { // DnsAddrs returns a slice of n random unique DNS addresses in the format // "xxxxxxxx.example.com:port". -func DnsAddrs(n int) []string { - const ( - nameLen = 8 - lowerAsciiA = 97 - ) +func (r *Random) DnsAddrs(n int) []string { + const nameLen = 8 addrs := make([]string, n) addrSet := make(map[string]struct{}, n) - rng := NewRand() for i := 0; i < n; i++ { - var name [nameLen]byte - for j := range nameLen { - name[j] = byte(rng.Intn(26) + lowerAsciiA) - } - addr := fmt.Sprintf("/dns4/%s.example.com/tcp/%d", name, rng.Intn(maxTcpPort-minTcpPort)+minTcpPort) + addr := fmt.Sprintf("/dns4/%s.example.com/tcp/%d", r.Name(nameLen), r.Intn(maxTcpPort-minTcpPort)+minTcpPort) if _, ok := addrSet[addr]; ok { i-- continue @@ -100,28 +100,27 @@ func DnsAddrs(n int) []string { } // BlocksOfSize generates a slice of blocks of the specified byte size. -func BlocksOfSize(n int, size int) []blocks.Block { +func (r *Random) BlocksOfSize(n int, size int) []blocks.Block { genBlocks := make([]blocks.Block, n) for i := range n { - genBlocks[i] = blocks.NewBlock(Bytes(size)) + genBlocks[i] = blocks.NewBlock(r.Bytes(size)) } return genBlocks } // Bytes returns a byte array of the given size with random values. -func Bytes(n int) []byte { +func (r *Random) Bytes(n int) []byte { data := make([]byte, n) - NewRand().Read(data) + r.Read(data) return data } // Cids returns a slice of n random unique CIDs. -func Cids(n int) []cid.Cid { +func (r *Random) Cids(n int) []cid.Cid { cids := make([]cid.Cid, 0, n) - rng := NewRand() for len(cids) < n { var b [32]byte - rng.Read(b[:]) + r.Read(b[:]) h, err := multihash.Encode(b[:], multihash.SHA2_256) if err != nil { panic(err) @@ -132,8 +131,8 @@ func Cids(n int) []cid.Cid { } // Identity returns a random unique peer ID, private key, and public key. -func Identity() (peer.ID, crypto.PrivKey, crypto.PubKey) { - privKey, pubKey, err := crypto.GenerateKeyPairWithReader(crypto.Ed25519, 256, NewRand()) +func (r *Random) Identity() (peer.ID, crypto.PrivKey, crypto.PubKey) { + privKey, pubKey, err := crypto.GenerateKeyPairWithReader(crypto.Ed25519, 256, r) if err != nil { panic(err) } @@ -144,56 +143,34 @@ func Identity() (peer.ID, crypto.PrivKey, crypto.PubKey) { return peerID, privKey, pubKey } -func multiaddrs(n int, addrsFunc func(int) []string) []multiaddr.Multiaddr { - addrs := addrsFunc(n) - maddrs := make([]multiaddr.Multiaddr, n) - for i, addr := range addrs { - maddr, err := multiaddr.NewMultiaddr(addr) - if err != nil { - panic(err) - } - maddrs[i] = maddr - } - return maddrs -} - -// Multiaddrs returns a slice of n random unique Multiaddrs with IPv4 addresses. -func Multiaddrs(n int) []multiaddr.Multiaddr { - return multiaddrs(n, Addrs) -} - -// DnsMultiaddrs returns a slice of n random unique Multiaddrs with DNS addresses. -func DnsMultiaddrs(n int) []multiaddr.Multiaddr { - return multiaddrs(n, DnsAddrs) +// Multiaddrs returns a slice of n random unique Multiaddrs with ipv4 addresses. +func (r *Random) Multiaddrs(n int) []multiaddr.Multiaddr { + return addrsToMultiaddrs(r.Addrs(n)) } -var httpMultiaddrComponent = multiaddr.StringCast("/http") - -func httpMultiaddrs(n int, multiaddrsFunc func(int) []multiaddr.Multiaddr) []multiaddr.Multiaddr { - maddrs := multiaddrsFunc(n) - for i, ma := range maddrs { - maddrs[i] = multiaddr.Join(ma, httpMultiaddrComponent) - } - return maddrs +// DnsMultiaddrs returns a slice of n random unique Multiaddrs with dns addresses. +func (r *Random) DnsMultiaddrs(n int) []multiaddr.Multiaddr { + return addrsToMultiaddrs(r.DnsAddrs(n)) } -// HttpMultiaddrs returns a slice of n random unique Multiaddrs. -func HttpMultiaddrs(n int) []multiaddr.Multiaddr { - return httpMultiaddrs(n, Multiaddrs) +// HttpMultiaddrs returns a slice of n random unique Multiaddrs with ipv4 +// addresses and http protocol. +func (r *Random) HttpMultiaddrs(n int) []multiaddr.Multiaddr { + return addrsToHttpMultiaddrs(r.Addrs(n)) } -// HttpDnsMultiaddrs returns a slice of n random unique Multiaddrs with DNS addresses. -func HttpDnsMultiaddrs(n int) []multiaddr.Multiaddr { - return httpMultiaddrs(n, DnsMultiaddrs) +// HttpDnsMultiaddrs returns a slice of n random unique Multiaddrs with dns +// addresses and http protocol. +func (r *Random) HttpDnsMultiaddrs(n int) []multiaddr.Multiaddr { + return addrsToHttpMultiaddrs(r.DnsAddrs(n)) } // Multihashes returns a slice of n random unique Multihashes. -func Multihashes(n int) []multihash.Multihash { - rng := NewRand() +func (r *Random) Multihashes(n int) []multihash.Multihash { mhashes := make([]multihash.Multihash, 0, n) for len(mhashes) < n { var b [32]byte - rng.Read(b[:]) + r.Read(b[:]) h, err := multihash.Encode(b[:], multihash.SHA2_256) if err != nil { panic(err.Error()) @@ -203,12 +180,40 @@ func Multihashes(n int) []multihash.Multihash { return mhashes } +// Name returns a string of n random lower-alphanumeric characters. +func (r *Random) Name(size int) string { + const alphanum = "abcdefghijklmnopqrstuvwxyz0123456789" + + if size <= 0 { + return "" + } + src := make([]byte, size) + r.Read(src) + for i := range src { + src[i] = alphanum[src[i]%36] + } + return string(src) +} + +// NameSize returns a string of random lower-alphanumeric characters, having a +// random size of at least minSize and at most maxSize. +func (r *Random) NameSize(minSize, maxSize int) string { + if minSize > maxSize { + minSize, maxSize = maxSize, minSize + } + sizeDiff := maxSize - minSize + size := minSize + if sizeDiff != 0 { + size += r.Intn(sizeDiff + 1) + } + return r.Name(size) +} + // Peers returns a slice of n random peer IDs. -func Peers(n int) []peer.ID { +func (r *Random) Peers(n int) []peer.ID { peerIDs := make([]peer.ID, n) - rng := NewRand() for i := range n { - _, publicKey, err := crypto.GenerateEd25519Key(rng) + _, publicKey, err := crypto.GenerateEd25519Key(r) if err != nil { panic(err) } @@ -221,44 +226,42 @@ func Peers(n int) []peer.ID { return peerIDs } -func addrInfos(numPeers, numAddrs int, multiaddrsFunc func(int) []multiaddr.Multiaddr) []peer.AddrInfo { - peerIDs := Peers(numPeers) - addrInfos := make([]peer.AddrInfo, numPeers) - for i := range numPeers { - addrInfos[i] = peer.AddrInfo{ - ID: peerIDs[i], - Addrs: multiaddrsFunc(numAddrs), - } - } - return addrInfos +// AddrInfos returns a slice AddrInfo with numPeers elements. Each AddrInfo has +// a unique ID and numAddrs Addresses. The multiaddrs are ipv4 addresses. +func (r *Random) AddrInfos(numPeers, numAddrs int) []peer.AddrInfo { + return r.addrInfos(numPeers, numAddrs, r.Multiaddrs) } -// AddrInfos returns a slice AddrInfo with numPeers elements. Each AddrInfo -// element will have a unique ID and numAddrs Addresses. The multiaddrs will be -// ipv4 addresses. -func AddrInfos(numPeers, numAddrs int) []peer.AddrInfo { - return addrInfos(numPeers, numAddrs, Multiaddrs) +// DnsAddrInfos returns a slice AddrInfo with numPeers elements. Each AddrInfo +// has a unique ID and numAddrs Addresses. The multiaddrs are dns addresses. +func (r *Random) DnsAddrInfos(numPeers, numAddrs int) []peer.AddrInfo { + return r.addrInfos(numPeers, numAddrs, r.DnsMultiaddrs) } -// AddrInfos returns a slice AddrInfo with numPeers elements. Each AddrInfo -// element will have a unique ID and numAddrs Addresses. The multiaddrs will be -// dns addresses. -func DnsAddrInfos(numPeers, numAddrs int) []peer.AddrInfo { - return addrInfos(numPeers, numAddrs, DnsMultiaddrs) +// HttpAddrInfos returns a slice AddrInfo with numPeers elements. Each AddrInfo +// has a unique ID and numAddrs Addresses. The multiaddrs are ipv4 addresses +// with http protocol +func (r *Random) HttpAddrInfos(numPeers, numAddrs int) []peer.AddrInfo { + return r.addrInfos(numPeers, numAddrs, r.HttpMultiaddrs) } -// AddrInfos returns a slice AddrInfo with numPeers elements. Each AddrInfo -// element will have a unique ID and numAddrs Addresses. The multiaddrs will be -// ipv4 addresses with http. -func HttpAddrInfos(numPeers, numAddrs int) []peer.AddrInfo { - return addrInfos(numPeers, numAddrs, HttpMultiaddrs) +// HttpDnsAddrInfos returns a slice AddrInfo with numPeers elements. Each +// AddrInfo has a unique ID and numAddrs Addresses. The multiaddrs are dns +// addresses with http protocol. +func (r *Random) HttpDnsAddrInfos(numPeers, numAddrs int) []peer.AddrInfo { + return r.addrInfos(numPeers, numAddrs, r.HttpDnsMultiaddrs) } -// AddrInfos returns a slice AddrInfo with numPeers elements. Each AddrInfo -// element will have a unique ID and numAddrs Addresses. The multiaddrs will be -// dns addresses with http. -func HttpDnsAddrInfos(numPeers, numAddrs int) []peer.AddrInfo { - return addrInfos(numPeers, numAddrs, HttpDnsMultiaddrs) +func (r *Random) addrInfos(numPeers, numAddrs int, multiaddrsFunc func(int) []multiaddr.Multiaddr) []peer.AddrInfo { + peerIDs := r.Peers(numPeers) + addrInfos := make([]peer.AddrInfo, numPeers) + for i := range numPeers { + addrInfos[i] = peer.AddrInfo{ + ID: peerIDs[i], + Addrs: multiaddrsFunc(numAddrs), + } + } + return addrInfos } // Sequence returns a series of monotonically increasing numbers, starting at @@ -286,3 +289,177 @@ func Sequence(n int) []uint64 { func SequenceNext() uint64 { return globalSeqGen.Add(1) } + +// NewRand returns a new pseudo-random number source, seeded with the next +// value of a global sequence. The returned Rand instance must not be used +// concurrently. +func NewRand() *rand.Rand { + return New().Rand +} + +// NewSeededRand returns a new pseudo-random number source seeded with the +// specified value. The returned Rand instance must not be used concurrently. +func NewSeededRand(seed int64) *rand.Rand { + return NewSeeded(seed).Rand +} + +// Addrs returns a slice of n random unique IPv4 addresses. +// +// Creates its own random source; safe for concurrent use. +func Addrs(n int) []string { + return New().Addrs(n) +} + +// DnsAddrs returns a slice of n random unique DNS addresses in the format +// "xxxxxxxx.example.com:port". +// +// Creates its own random source; safe for concurrent use. +func DnsAddrs(n int) []string { + return New().DnsAddrs(n) +} + +// BlocksOfSize generates a slice of blocks of the specified byte size. +// +// Creates its own random source; safe for concurrent use. +func BlocksOfSize(n int, size int) []blocks.Block { + return New().BlocksOfSize(n, size) +} + +// Bytes returns a byte array of the given size with random values. +// +// Creates its own random source; safe for concurrent use. +func Bytes(n int) []byte { + return New().Bytes(n) +} + +// Cids returns a slice of n random unique CIDs. +// +// Creates its own random source; safe for concurrent use. +func Cids(n int) []cid.Cid { + return New().Cids(n) +} + +// Identity returns a random unique peer ID, private key, and public key. +// +// Creates its own random source; safe for concurrent use. +func Identity() (peer.ID, crypto.PrivKey, crypto.PubKey) { + return New().Identity() +} + +// Multiaddrs returns a slice of n random unique Multiaddrs with ipv4 addresses. +// +// Creates its own random source; safe for concurrent use. +func Multiaddrs(n int) []multiaddr.Multiaddr { + return New().Multiaddrs(n) +} + +// DnsMultiaddrs returns a slice of n random unique Multiaddrs with dns addresses. +// +// Creates its own random source; safe for concurrent use. +func DnsMultiaddrs(n int) []multiaddr.Multiaddr { + return New().DnsMultiaddrs(n) +} + +// HttpMultiaddrs returns a slice of n random unique Multiaddrs with ipv4 +// addresses and http protocol. +// +// Creates its own random source; safe for concurrent use. +func HttpMultiaddrs(n int) []multiaddr.Multiaddr { + return New().HttpMultiaddrs(n) +} + +// HttpDnsMultiaddrs returns a slice of n random unique Multiaddrs with dns +// addresses and http protocol. +// +// Creates its own random source; safe for concurrent use. +func HttpDnsMultiaddrs(n int) []multiaddr.Multiaddr { + return New().HttpDnsMultiaddrs(n) +} + +// Multihashes returns a slice of n random unique Multihashes. +// +// Creates its own random source; safe for concurrent use. +func Multihashes(n int) []multihash.Multihash { + return New().Multihashes(n) +} + +// Name returns a string of n random lower-alphanumeric characters. +// +// Creates its own random source; safe for concurrent use. +func Name(size int) string { + return New().Name(size) +} + +// NameSize returns a string of random lower-alphanumeric characters, having a +// random size of at least minSize and at most maxSize. +// +// Creates its own random source; safe for concurrent use. +func NameSize(minSize, maxSize int) string { + return New().NameSize(minSize, maxSize) +} + +// Peers returns a slice of n random peer IDs. +// +// Creates its own random source; safe for concurrent use. +func Peers(n int) []peer.ID { + return New().Peers(n) +} + +// AddrInfos returns a slice AddrInfo with numPeers elements. Each AddrInfo has +// a unique ID and numAddrs Addresses. The multiaddrs are ipv4 addresses. +// +// Creates its own random source; safe for concurrent use. +func AddrInfos(numPeers, numAddrs int) []peer.AddrInfo { + return New().AddrInfos(numPeers, numAddrs) +} + +// DnsAddrInfos returns a slice AddrInfo with numPeers elements. Each AddrInfo +// has a unique ID and numAddrs Addresses. The multiaddrs are dns addresses. +// +// Creates its own random source; safe for concurrent use. +func DnsAddrInfos(numPeers, numAddrs int) []peer.AddrInfo { + return New().DnsAddrInfos(numPeers, numAddrs) +} + +// HttpAddrInfos returns a slice AddrInfo with numPeers elements. Each AddrInfo +// has a unique ID and numAddrs Addresses. The multiaddrs are ipv4 addresses +// with http protocol +// +// Creates its own random source; safe for concurrent use. +func HttpAddrInfos(numPeers, numAddrs int) []peer.AddrInfo { + return New().HttpAddrInfos(numPeers, numAddrs) +} + +// HttpDnsAddrInfos returns a slice AddrInfo with numPeers elements. Each +// AddrInfo has a unique ID and numAddrs Addresses. The multiaddrs are dns +// addresses with http protocol. +// +// Creates its own random source; safe for concurrent use. +func HttpDnsAddrInfos(numPeers, numAddrs int) []peer.AddrInfo { + return New().HttpDnsAddrInfos(numPeers, numAddrs) +} + +func addrsToMultiaddrs(addrs []string) []multiaddr.Multiaddr { + maddrs := make([]multiaddr.Multiaddr, len(addrs)) + for i, addr := range addrs { + maddr, err := multiaddr.NewMultiaddr(addr) + if err != nil { + panic(err) + } + maddrs[i] = maddr + } + return maddrs +} + +func addrsToHttpMultiaddrs(addrs []string) []multiaddr.Multiaddr { + httpMultiaddrComponent := multiaddr.StringCast("/http") + maddrs := make([]multiaddr.Multiaddr, len(addrs)) + for i, addr := range addrs { + maddr, err := multiaddr.NewMultiaddr(addr) + if err != nil { + panic(err) + } + maddrs[i] = multiaddr.Join(maddr, httpMultiaddrComponent) + } + return maddrs +} diff --git a/random/random_test.go b/random/random_test.go index 208b72f..50a459f 100644 --- a/random/random_test.go +++ b/random/random_test.go @@ -8,10 +8,23 @@ import ( blocks "github.com/ipfs/go-block-format" "github.com/ipfs/go-test/random" "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multiaddr" "github.com/multiformats/go-multihash" "github.com/stretchr/testify/require" ) +func TestAddrs(t *testing.T) { + addrs := random.Addrs(3) + require.Len(t, addrs, 3) + for _, a := range addrs { + require.True(t, strings.HasPrefix(a, "/ip4/")) + require.Equal(t, 3, strings.Count(a, ".")) + require.Contains(t, a, "/tcp/") + _, err := multiaddr.NewMultiaddr(a) + require.NoError(t, err) + } +} + func TestBytes(t *testing.T) { b1 := random.Bytes(32) b2 := random.Bytes(32) @@ -31,6 +44,18 @@ func TestBlocksOfSize(t *testing.T) { } } +func TestDnsAddrs(t *testing.T) { + addrs := random.DnsAddrs(3) + require.Len(t, addrs, 3) + for _, a := range addrs { + require.True(t, strings.HasPrefix(a, "/dns4/")) + require.Equal(t, 2, strings.Count(a, ".")) + require.Contains(t, a, ".example.com/tcp/") + _, err := multiaddr.NewMultiaddr(a) + require.NoError(t, err) + } +} + func TestMultiaddrs(t *testing.T) { const n = 2000 ms := random.Multiaddrs(n) @@ -59,11 +84,76 @@ func TestHttpMultiaddrs(t *testing.T) { require.Len(t, hms, 3) for _, ma := range hms { maStr := ma.String() - require.True(t, strings.HasSuffix(maStr, "http")) + require.True(t, strings.HasPrefix(maStr, "/ip4/")) + require.True(t, strings.HasSuffix(maStr, "/http")) t.Log("http multiaddr:", maStr) } } +func TestHttpDnsMultiaddrs(t *testing.T) { + hdms := random.HttpDnsMultiaddrs(3) + require.Len(t, hdms, 3) + for _, ma := range hdms { + maStr := ma.String() + require.True(t, strings.HasPrefix(maStr, "/dns4/")) + require.True(t, strings.HasSuffix(maStr, "/http")) + t.Log("http multiaddr:", maStr) + } +} + +func TestAddrInfos(t *testing.T) { + const ( + numPeers = 3 + numAddrs = 2 + ) + addrInfos := random.AddrInfos(numPeers, numAddrs) + require.Len(t, addrInfos, numPeers) + for _, addrInfo := range addrInfos { + require.Len(t, addrInfo.Addrs, numAddrs) + for _, addr := range addrInfo.Addrs { + maStr := addr.String() + require.True(t, strings.HasPrefix(maStr, "/ip4/")) + require.False(t, strings.HasSuffix(maStr, "/http")) + } + t.Log("AddrInfo:", addrInfo) + } +} + +func TestDnsAddrInfos(t *testing.T) { + const ( + numPeers = 3 + numAddrs = 2 + ) + addrInfos := random.DnsAddrInfos(numPeers, numAddrs) + require.Len(t, addrInfos, numPeers) + for _, addrInfo := range addrInfos { + require.Len(t, addrInfo.Addrs, numAddrs) + for _, addr := range addrInfo.Addrs { + maStr := addr.String() + require.True(t, strings.HasPrefix(maStr, "/dns4/")) + require.False(t, strings.HasSuffix(maStr, "/http")) + } + t.Log("AddrInfo:", addrInfo) + } +} + +func TestHttpAddrInfos(t *testing.T) { + const ( + numPeers = 3 + numAddrs = 2 + ) + addrInfos := random.HttpAddrInfos(numPeers, numAddrs) + require.Len(t, addrInfos, numPeers) + for _, addrInfo := range addrInfos { + require.Len(t, addrInfo.Addrs, numAddrs) + for _, addr := range addrInfo.Addrs { + maStr := addr.String() + require.True(t, strings.HasPrefix(maStr, "/ip4/")) + require.True(t, strings.HasSuffix(maStr, "/http")) + } + t.Log("AddrInfo:", addrInfo) + } +} func TestHttpDnsAddrInfos(t *testing.T) { const ( numPeers = 3 @@ -73,9 +163,11 @@ func TestHttpDnsAddrInfos(t *testing.T) { require.Len(t, addrInfos, numPeers) for _, addrInfo := range addrInfos { require.Len(t, addrInfo.Addrs, numAddrs) - maStr := addrInfo.Addrs[0].String() - require.True(t, strings.HasPrefix(maStr, "/dns4/")) - require.True(t, strings.HasSuffix(maStr, "http")) + for _, addr := range addrInfo.Addrs { + maStr := addr.String() + require.True(t, strings.HasPrefix(maStr, "/dns4/")) + require.True(t, strings.HasSuffix(maStr, "/http")) + } t.Log("AddrInfo:", addrInfo) } } @@ -109,6 +201,48 @@ func TestMultihashes(t *testing.T) { } } +func TestName(t *testing.T) { + const nameLen = 8 + var prevName string + for range 3 { + name := random.Name(nameLen) + require.Len(t, name, nameLen) + if prevName != "" { + require.NotEqual(t, prevName, name) + } + prevName = name + } +} + +func TestNameSize(t *testing.T) { + const ( + minSize = 4 + maxSize = 16 + maxIters = (maxSize - minSize) * 1000 + ) + wantSizes := make(map[int]struct{}, maxSize-minSize) + for i := minSize; i <= maxSize; i++ { + wantSizes[i] = struct{}{} + } + var prevName string + var iters int + for len(wantSizes) != 0 { + name := random.NameSize(minSize, maxSize) + require.GreaterOrEqual(t, len(name), minSize) + require.LessOrEqual(t, len(name), maxSize) + delete(wantSizes, len(name)) + if prevName != "" { + require.NotEqual(t, prevName, name) + } + prevName = name + if iters == maxIters { + t.Fatalf("did not generate all name sizes within %d tries", maxIters) + } + iters++ + } + t.Log("got all sizes in", iters, "iterarions") +} + func TestPeers(t *testing.T) { peerIDs := random.Peers(3) require.Len(t, peerIDs, 3) From ef976ede356449899474b04c32e2d8552c868cd9 Mon Sep 17 00:00:00 2001 From: gammazero <11790789+gammazero@users.noreply.github.com> Date: Thu, 9 Jul 2026 05:15:15 -1000 Subject: [PATCH 2/9] API Changes - Upgrade to math/rand/v2 - Seed value changed from uint64 to [32]byte as needed for ChaCha8 - Added Name and NameSize functions to API - Do not support global seed - Do not support global sequence - Remove NewRand and NewSeededRand - Make sequence a separate type - Added NewSeed function to create new seed. - Added MakeSeed function to create seed from uint64 The move to a different number generator, ChaCha8, means that different values will be generated than before, and tests depending on deterministic random values need to be updated. --- random/files/files.go | 37 ++++++------ random/random.go | 126 ++++++++++++++++------------------------- random/random_test.go | 127 ++++++++++++++++-------------------------- 3 files changed, 115 insertions(+), 175 deletions(-) diff --git a/random/files/files.go b/random/files/files.go index 81e3c48..ad275e9 100644 --- a/random/files/files.go +++ b/random/files/files.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "io" - "math/rand" "os" "path/filepath" @@ -42,9 +41,9 @@ type Config struct { // RandomSize specifies whether or not to randomize the file size from 1 to // the value configured by FileSize. RandomSize bool - // Seed sets the seen for the random number generator when set to a - // non-zero value. - Seed int64 + // Seed sets the seed for the random number generator when set to a + // non-empty value. + Seed []byte } // DefaultConfig returns default settings for creating random files and @@ -75,11 +74,13 @@ func Create(cfg Config, roots ...string) error { return err } - var rnd *rand.Rand - if cfg.Seed == 0 { - rnd = random.NewRand() + var rnd *random.Random + if len(cfg.Seed) == 0 { + rnd = random.New() } else { - rnd = random.NewSeededRand(cfg.Seed) + var seed [32]byte + copy(seed[:], cfg.Seed) + rnd = random.NewSeeded(seed) } for _, root := range roots { @@ -128,7 +129,7 @@ func RandomName(sizes ...int) string { cfg = DefaultConfig() } - return cfg.randomName(random.NewRand()) + return cfg.randomName(random.New()) } func (cfg *Config) validate() error { @@ -165,11 +166,11 @@ func validateNameSize(minSize, maxSize int) error { return nil } -func (cfg *Config) writeTree(rnd *rand.Rand, root string, depth int) error { +func (cfg *Config) writeTree(rnd *random.Random, root string, depth int) error { nFiles := cfg.Files if nFiles != 0 { if cfg.RandomFiles && nFiles > 1 { - nFiles = rnd.Intn(nFiles) + 1 + nFiles = rnd.IntN(nFiles) + 1 } for i := 0; i < nFiles; i++ { @@ -182,7 +183,7 @@ func (cfg *Config) writeTree(rnd *rand.Rand, root string, depth int) error { return cfg.writeSubdirs(rnd, root, depth) } -func (cfg *Config) writeSubdirs(rnd *rand.Rand, root string, depth int) error { +func (cfg *Config) writeSubdirs(rnd *random.Random, root string, depth int) error { if depth == cfg.Depth { return nil } @@ -190,7 +191,7 @@ func (cfg *Config) writeSubdirs(rnd *rand.Rand, root string, depth int) error { nDirs := cfg.Dirs if cfg.RandomDirs && nDirs > 1 { - nDirs = rnd.Intn(nDirs) + 1 + nDirs = rnd.IntN(nDirs) + 1 } for i := 0; i < nDirs; i++ { @@ -202,7 +203,7 @@ func (cfg *Config) writeSubdirs(rnd *rand.Rand, root string, depth int) error { return nil } -func (cfg *Config) writeSubdir(rnd *rand.Rand, root string, depth int) error { +func (cfg *Config) writeSubdir(rnd *random.Random, root string, depth int) error { name := cfg.randomName(rnd) root = filepath.Join(root, name) if err := os.MkdirAll(root, 0755); err != nil { @@ -216,11 +217,11 @@ func (cfg *Config) writeSubdir(rnd *rand.Rand, root string, depth int) error { return cfg.writeTree(rnd, root, depth) } -func (cfg *Config) randomName(rnd *rand.Rand) string { +func (cfg *Config) randomName(rnd *random.Random) string { sizeDiff := cfg.NameMaxSize - cfg.NameMinSize size := cfg.NameMinSize if sizeDiff != 0 { - size += rnd.Intn(sizeDiff + 1) + size += rnd.IntN(sizeDiff + 1) } if size <= 0 { return "" @@ -233,7 +234,7 @@ func (cfg *Config) randomName(rnd *rand.Rand) string { return string(src) } -func (cfg *Config) writeFile(rnd *rand.Rand, root string) error { +func (cfg *Config) writeFile(rnd *random.Random, root string) error { name := cfg.randomName(rnd) filePath := filepath.Join(root, name) f, err := os.Create(filePath) @@ -244,7 +245,7 @@ func (cfg *Config) writeFile(rnd *rand.Rand, root string) error { if cfg.FileSize > 0 { fileSize := cfg.FileSize if cfg.RandomSize && fileSize > 1 { - fileSize = rnd.Int63n(fileSize) + 1 + fileSize = rnd.Int64N(fileSize) + 1 } if _, err := io.CopyN(f, rnd, fileSize); err != nil { diff --git a/random/random.go b/random/random.go index 8dcb34d..ff0a6ee 100644 --- a/random/random.go +++ b/random/random.go @@ -1,10 +1,10 @@ package random import ( + crand "crypto/rand" + "encoding/binary" "fmt" - "math/rand" - "sync/atomic" - "time" + "math/rand/v2" blocks "github.com/ipfs/go-block-format" "github.com/ipfs/go-cid" @@ -15,35 +15,24 @@ import ( "github.com/multiformats/go-multihash" ) -var ( - initSeed int64 - globalSeed atomic.Int64 - globalSeqGen atomic.Uint64 -) - const ( minTcpPort = 1024 maxTcpPort = 65535 ) -func init() { - SetSeed(time.Now().UTC().UnixNano()) -} - -// Returns the initial seed used for the pseudo-random number generator, or the -// most recent value set by SetSeed. -func Seed() int64 { - return initSeed +// NewSeed creates a new random seed for use in seeding creating random sources +// with the same seed value using NewSeeded. +func NewSeed() [32]byte { + var seed [32]byte + _, _ = crand.Read(seed[:]) + return seed } -// Sets the seed for the pseudo-random number generator. Calling -// SetSeed(Seed()) each time before generating random items will cause items -// with the same values to be generated. -func SetSeed(seed int64) { - initSeed = seed - globalSeed.Store(seed) - rng := rand.New(rand.NewSource(seed)) - globalSeqGen.Store(rng.Uint64()) +// MakeSeed creates a seed value from a uint64 value. +func MakeSeed(val uint64) [32]byte { + var seed [32]byte + binary.BigEndian.PutUint64(seed[:], val) + return seed } // Random contains its own pseudo-random source. The same Random instance must @@ -51,28 +40,50 @@ func SetSeed(seed int64) { // concurrent goroutines. type Random struct { *rand.Rand + cc8 *rand.ChaCha8 } -// New returns a new Random instance that contains its own pseudo-random -// number source, seeded with the next value of a global sequence. +// New returns a new Random instance that contains its own pseudo-random number +// source, seeded with a random value. func New() *Random { - return NewSeeded(globalSeed.Add(1)) + var seed [32]byte + _, _ = crand.Read(seed[:]) + cc8 := rand.NewChaCha8(seed) + + return &Random{ + cc8: cc8, + Rand: rand.New(cc8), + } } // NewSeeded returns a new Random instance that contains its own pseudo-random -// number source seeded with the specified value. -func NewSeeded(seed int64) *Random { +// number source seeded with the specified value. Creating a random source +// using the same seed value causes the generator to deterministically produce +// the same sequence of pseudo-random numbers. +func NewSeeded(seed [32]byte) *Random { + cc8 := rand.NewChaCha8(seed) return &Random{ - Rand: rand.New(rand.NewSource(seed)), + Rand: rand.New(cc8), + cc8: cc8, } } +// Read reads exactly len(p) bytes into p. It always returns len(p) and a nil error. +func (r *Random) Read(p []byte) (n int, err error) { + return r.cc8.Read(p) +} + +// Reset resets the Random instance to behave the same way as NewSeeded(seed). +func (r *Random) Reset(seed [32]byte) { + r.cc8.Seed(seed) +} + // Addrs returns a slice of n random unique IPv4 addresses. func (r *Random) Addrs(n int) []string { addrs := make([]string, n) addrSet := make(map[string]struct{}, n) for i := 0; i < n; i++ { - addr := fmt.Sprintf("/ip4/%d.%d.%d.%d/tcp/%d", r.Intn(254)+1, r.Intn(254)+1, r.Intn(254)+1, r.Intn(254)+1, r.Intn(maxTcpPort-minTcpPort)+minTcpPort) + addr := fmt.Sprintf("/ip4/%d.%d.%d.%d/tcp/%d", r.IntN(254)+1, r.IntN(254)+1, r.IntN(254)+1, r.IntN(254)+1, r.IntN(maxTcpPort-minTcpPort)+minTcpPort) if _, ok := addrSet[addr]; ok { i-- continue @@ -89,7 +100,7 @@ func (r *Random) DnsAddrs(n int) []string { addrs := make([]string, n) addrSet := make(map[string]struct{}, n) for i := 0; i < n; i++ { - addr := fmt.Sprintf("/dns4/%s.example.com/tcp/%d", r.Name(nameLen), r.Intn(maxTcpPort-minTcpPort)+minTcpPort) + addr := fmt.Sprintf("/dns4/%s.example.com/tcp/%d", r.Name(nameLen), r.IntN(maxTcpPort-minTcpPort)+minTcpPort) if _, ok := addrSet[addr]; ok { i-- continue @@ -100,7 +111,7 @@ func (r *Random) DnsAddrs(n int) []string { } // BlocksOfSize generates a slice of blocks of the specified byte size. -func (r *Random) BlocksOfSize(n int, size int) []blocks.Block { +func (r *Random) BlocksOfSize(n int, size int64) []blocks.Block { genBlocks := make([]blocks.Block, n) for i := range n { genBlocks[i] = blocks.NewBlock(r.Bytes(size)) @@ -109,7 +120,7 @@ func (r *Random) BlocksOfSize(n int, size int) []blocks.Block { } // Bytes returns a byte array of the given size with random values. -func (r *Random) Bytes(n int) []byte { +func (r *Random) Bytes(n int64) []byte { data := make([]byte, n) r.Read(data) return data @@ -204,7 +215,7 @@ func (r *Random) NameSize(minSize, maxSize int) string { sizeDiff := maxSize - minSize size := minSize if sizeDiff != 0 { - size += r.Intn(sizeDiff + 1) + size += r.IntN(sizeDiff + 1) } return r.Name(size) } @@ -264,45 +275,6 @@ func (r *Random) addrInfos(numPeers, numAddrs int, multiaddrsFunc func(int) []mu return addrInfos } -// Sequence returns a series of monotonically increasing numbers, starting at -// the next unique global sequence value. Any current calls to Sequence will -// not generate any overlapping values. -// -// The sequence numbers themselves are not random, only the global starting -// value of the sequence numbers is random. This ensures that all sequences -// generated within a test are unique, assuming < 2^64 values are generated, -// but start out at a random value. -func Sequence(n int) []uint64 { - if n == 1 { - return []uint64{globalSeqGen.Add(1)} - } - seq := make([]uint64, n) - seqVal := globalSeqGen.Add(uint64(n)) - uint64(n-1) - for i := range n { - seq[i] = seqVal + uint64(i) - } - return seq -} - -// SequenceNext returns the next unique global sequence value. This is -// equivalent to Sequence(1)[0]. -func SequenceNext() uint64 { - return globalSeqGen.Add(1) -} - -// NewRand returns a new pseudo-random number source, seeded with the next -// value of a global sequence. The returned Rand instance must not be used -// concurrently. -func NewRand() *rand.Rand { - return New().Rand -} - -// NewSeededRand returns a new pseudo-random number source seeded with the -// specified value. The returned Rand instance must not be used concurrently. -func NewSeededRand(seed int64) *rand.Rand { - return NewSeeded(seed).Rand -} - // Addrs returns a slice of n random unique IPv4 addresses. // // Creates its own random source; safe for concurrent use. @@ -321,14 +293,14 @@ func DnsAddrs(n int) []string { // BlocksOfSize generates a slice of blocks of the specified byte size. // // Creates its own random source; safe for concurrent use. -func BlocksOfSize(n int, size int) []blocks.Block { +func BlocksOfSize(n int, size int64) []blocks.Block { return New().BlocksOfSize(n, size) } // Bytes returns a byte array of the given size with random values. // // Creates its own random source; safe for concurrent use. -func Bytes(n int) []byte { +func Bytes(n int64) []byte { return New().Bytes(n) } diff --git a/random/random_test.go b/random/random_test.go index 50a459f..7fa21bf 100644 --- a/random/random_test.go +++ b/random/random_test.go @@ -2,7 +2,6 @@ package random_test import ( "strings" - "sync" "testing" blocks "github.com/ipfs/go-block-format" @@ -253,99 +252,67 @@ func TestPeers(t *testing.T) { } } -// TestSeed tests that setting seed back to its initial value generates the -// same results. -func TestSeed(t *testing.T) { - initSeed := random.Seed() +func TestReset(t *testing.T) { + seed := random.NewSeed() - random.SetSeed(initSeed) - b1 := random.Bytes(32) - firstNum := random.SequenceNext() + rnd := random.NewSeeded(seed) + b1 := rnd.Bytes(32) + num1 := rnd.Int() + name1 := rnd.NameSize(5, 10) - random.SetSeed(initSeed) - b2 := random.Bytes(32) - secondNum := random.SequenceNext() + b2 := rnd.Bytes(32) + num2 := rnd.Int() + name2 := rnd.NameSize(5, 10) - require.Equal(t, b1, b2) - require.Equal(t, firstNum, secondNum) -} + rnd.Reset(seed) -func TestSequence(t *testing.T) { - const ( - seqCount = 5 - seqSize = 10 - ) - firstNum := random.SequenceNext() - secondNum := random.Sequence(1)[0] - require.Equal(t, firstNum+1, secondNum) - - seen := make(map[uint64]struct{}, seqSize*seqCount+1) - seen[firstNum] = struct{}{} - seen[secondNum] = struct{}{} - - seqs := make(chan []uint64) - startGate := make(chan struct{}) - var ready sync.WaitGroup - ready.Add(seqCount) - - for range seqCount { - go func() { - ready.Done() - <-startGate - seq := random.Sequence(seqSize) - seqs <- seq - }() - } + b3 := rnd.Bytes(32) + num3 := rnd.Int() + name3 := rnd.NameSize(5, 10) - ready.Wait() - close(startGate) + require.NotEqual(t, b1, b2) + require.NotEqual(t, num1, num2) + require.NotEqual(t, name1, name2) - for range seqCount { - seq := <-seqs - for _, num := range seq { - _, found := seen[num] - require.Falsef(t, found, "sequence number %d is not unique", num) - seen[num] = struct{}{} - } - } - lastNum := random.SequenceNext() - t.Log("first seq num:", firstNum) - t.Log("last seq num: ", lastNum) - require.Equal(t, secondNum+(seqCount*seqSize)+1, lastNum, "expected lastnum to be %d + %d + 1, was %d", secondNum, seqCount*seqSize, lastNum) + require.Equal(t, b1, b3) + require.Equal(t, num1, num3) + require.Equal(t, name1, name3) } -func TestNewRand(t *testing.T) { - rng1 := random.NewSeededRand(137) - rng2 := random.NewSeededRand(137) - allEqual := true - for range 100 { +func TestNewSeeded(t *testing.T) { + seed := random.NewSeed() + buf1 := make([]byte, 16) + buf2 := make([]byte, 16) + + rng1 := random.NewSeeded(seed) + rng2 := random.NewSeeded(seed) + for range 10 { n1 := rng1.Int() n2 := rng2.Int() - if n1 != n2 { - allEqual = false - break - } + require.Equal(t, n1, n2) + + name1 := rng1.NameSize(5, 10) + name2 := rng2.NameSize(5, 10) + require.Equal(t, name1, name2) + + rng1.Read(buf1) + rng2.Read(buf2) + require.Equal(t, buf1, buf2) } - require.True(t, allEqual) - rng1 = random.NewRand() - rng2 = random.NewRand() - for range 100 { + rng1 = random.New() + rng2 = random.New() + for range 10 { n1 := rng1.Int() n2 := rng2.Int() - if n1 != n2 { - allEqual = false - break - } - } - require.False(t, allEqual) -} + require.NotEqual(t, n1, n2) -func TestRead(t *testing.T) { - buf1 := make([]byte, 16) - buf2 := make([]byte, 16) + name1 := rng1.NameSize(5, 10) + name2 := rng2.NameSize(5, 10) + require.NotEqual(t, name1, name2) - random.NewRand().Read(buf1) - random.NewRand().Read(buf2) - require.NotEqual(t, buf1, buf2) + rng1.Read(buf1) + rng2.Read(buf2) + require.NotEqual(t, buf1, buf2) + } } From f4de4addda9d0aa6899aa8e163a6516447c82c3e Mon Sep 17 00:00:00 2001 From: gammazero <11790789+gammazero@users.noreply.github.com> Date: Thu, 9 Jul 2026 05:30:22 -1000 Subject: [PATCH 3/9] Add sequence --- random/sequence.go | 48 ++++++++++++++++++++++++++++++++++++ random/sequence_test.go | 54 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 random/sequence.go create mode 100644 random/sequence_test.go diff --git a/random/sequence.go b/random/sequence.go new file mode 100644 index 0000000..11072ae --- /dev/null +++ b/random/sequence.go @@ -0,0 +1,48 @@ +package random + +import ( + "math/rand/v2" + "sync/atomic" +) + +// Sequence returns a series of monotonically increasing numbers, starting at +// the next unique global sequence value. Any current calls to Sequence will +// not generate any overlapping values. +// +// The sequence numbers themselves are not random, only the global starting +// value of the sequence numbers is random. This ensures that all sequences +// generated within a test are unique, assuming < 2^64 values are generated, +// but start out at a random value. +type Sequence struct { + next atomic.Uint64 +} + +// NewSequence creates a new Sequence that starts at a random number. +func NewSequence() *Sequence { + return NewSequenceAt(rand.Uint64()) +} + +// NewSequenceAt creates a new Sequence that starts at a the specified number. +func NewSequenceAt(start uint64) *Sequence { + s := new(Sequence) + s.next.Store(start) + return s +} + +// NextN returns the next n sequence values. +func (s *Sequence) NextN(n int) []uint64 { + if n == 1 { + return []uint64{s.next.Add(1)} + } + seqVal := s.next.Add(uint64(n)) - uint64(n-1) + seq := make([]uint64, n) + for i := range n { + seq[i] = seqVal + uint64(i) + } + return seq +} + +// Next returns the next sequence value. This is equivalent to NextN(1)[0]. +func (s *Sequence) Next() uint64 { + return s.next.Add(1) +} diff --git a/random/sequence_test.go b/random/sequence_test.go new file mode 100644 index 0000000..482a553 --- /dev/null +++ b/random/sequence_test.go @@ -0,0 +1,54 @@ +package random_test + +import ( + "sync" + "testing" + + "github.com/ipfs/go-test/random" + "github.com/stretchr/testify/require" +) + +func TestSequence(t *testing.T) { + const ( + seqCount = 5 + seqSize = 10 + ) + seq := random.NewSequence() + + firstNum := seq.Next() + secondNum := seq.NextN(1)[0] + require.Equal(t, firstNum+1, secondNum) + + seen := make(map[uint64]struct{}, seqSize*seqCount+1) + seen[firstNum] = struct{}{} + seen[secondNum] = struct{}{} + + seqs := make(chan []uint64) + startGate := make(chan struct{}) + var ready sync.WaitGroup + ready.Add(seqCount) + + for range seqCount { + go func() { + ready.Done() + <-startGate + seqs <- seq.NextN(seqSize) + }() + } + + ready.Wait() + close(startGate) + + for range seqCount { + nums := <-seqs + for _, num := range nums { + _, found := seen[num] + require.Falsef(t, found, "sequence number %d is not unique", num) + seen[num] = struct{}{} + } + } + lastNum := seq.Next() + t.Log("first seq num:", firstNum) + t.Log("last seq num: ", lastNum) + require.Equal(t, secondNum+(seqCount*seqSize)+1, lastNum, "expected lastnum to be %d + %d + 1, was %d", secondNum, seqCount*seqSize, lastNum) +} From 27b54c53fe50a36e1f9d61774bc58f3bc0ead414 Mon Sep 17 00:00:00 2001 From: gammazero <11790789+gammazero@users.noreply.github.com> Date: Thu, 9 Jul 2026 06:05:26 -1000 Subject: [PATCH 4/9] update cli tools --- cli/random-data/main.go | 13 ++++++------- cli/random-files/main.go | 7 ++++++- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/cli/random-data/main.go b/cli/random-data/main.go index 95d4489..760d0e8 100644 --- a/cli/random-data/main.go +++ b/cli/random-data/main.go @@ -6,7 +6,6 @@ import ( "flag" "fmt" "io" - "math/rand" "os" random "github.com/ipfs/go-test/random" @@ -29,11 +28,11 @@ OPTIONS: var ( b64 bool - seed int64 + seed uint64 size int64 ) flag.BoolVar(&b64, "b64", false, "base-64 encode output") - flag.Int64Var(&seed, "seed", 0, "random seed, 0 or unset for current time") + flag.Uint64Var(&seed, "seed", 0, "random seed, 0 or unset for current time") flag.Int64Var(&size, "size", 0, "number of bytes to generate") flag.Parse() @@ -54,12 +53,12 @@ OPTIONS: } } -func writeData(seed, size int64, b64 bool) error { - var rnd *rand.Rand +func writeData(seed uint64, size int64, b64 bool) error { + var rnd *random.Random if seed == 0 { - rnd = random.NewRand() + rnd = random.New() } else { - rnd = random.NewSeededRand(seed) + rnd = random.NewSeeded(random.MakeSeed(seed)) } var w io.Writer diff --git a/cli/random-files/main.go b/cli/random-files/main.go index 0c8ca0a..3ed77b7 100644 --- a/cli/random-files/main.go +++ b/cli/random-files/main.go @@ -26,6 +26,7 @@ OPTIONS: var ( quiet bool paths []string + seed string ) cfg := files.DefaultConfig() @@ -38,7 +39,7 @@ OPTIONS: flag.BoolVar(&cfg.RandomDirs, "random-dirs", cfg.RandomDirs, "randomize number of subdirectories, from 1 to -dirs") flag.BoolVar(&cfg.RandomFiles, "random-files", cfg.RandomFiles, "randomize number of files, from 1 to -files") flag.BoolVar(&cfg.RandomSize, "random-size", cfg.RandomSize, "randomize file size, from 1 to -filesize") - flag.Int64Var(&cfg.Seed, "seed", cfg.Seed, "random seed, 0 for current time") + flag.StringVar(&seed, "seed", "", "random seed, leave empty to use random value") flag.Parse() paths = flag.Args() @@ -47,6 +48,10 @@ OPTIONS: cfg.Out = os.Stdout } + if len(seed) != 0 { + cfg.Seed = []byte(seed) + } + err := files.Create(cfg, paths...) if err != nil { fmt.Fprintln(os.Stderr, "error:", err) From 19fc05678b707072e689a20aff1976cafd256b71 Mon Sep 17 00:00:00 2001 From: gammazero <11790789+gammazero@users.noreply.github.com> Date: Thu, 9 Jul 2026 14:38:12 -1000 Subject: [PATCH 5/9] add NewRand and NewSeededRand for backward compatibility --- random/random.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/random/random.go b/random/random.go index ff0a6ee..2a40375 100644 --- a/random/random.go +++ b/random/random.go @@ -435,3 +435,13 @@ func addrsToHttpMultiaddrs(addrs []string) []multiaddr.Multiaddr { } return maddrs } + +// Deprecated: use New() +func NewRand() *Random { + return New() +} + +// Deprecated: use NewSeeded(bytesSeed) or NewSeeded(MakeSeed(uint64Seed)) +func NewSeededRand(seed uint64) *Random { + return NewSeeded(MakeSeed(seed)) +} From 0692bf030b3b9e2d7717bc6a60db1874f59dfb41 Mon Sep 17 00:00:00 2001 From: gammazero <11790789+gammazero@users.noreply.github.com> Date: Sat, 11 Jul 2026 09:34:23 -1000 Subject: [PATCH 6/9] Rename Reset to Seed to be consistent with ChaCha8 source --- random/random.go | 4 ++-- random/random_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/random/random.go b/random/random.go index 2a40375..7330fdc 100644 --- a/random/random.go +++ b/random/random.go @@ -73,8 +73,8 @@ func (r *Random) Read(p []byte) (n int, err error) { return r.cc8.Read(p) } -// Reset resets the Random instance to behave the same way as NewSeeded(seed). -func (r *Random) Reset(seed [32]byte) { +// Seed resets the Random instance to behave the same way as NewSeeded(seed). +func (r *Random) Seed(seed [32]byte) { r.cc8.Seed(seed) } diff --git a/random/random_test.go b/random/random_test.go index 7fa21bf..2c976d4 100644 --- a/random/random_test.go +++ b/random/random_test.go @@ -252,7 +252,7 @@ func TestPeers(t *testing.T) { } } -func TestReset(t *testing.T) { +func TestSeed(t *testing.T) { seed := random.NewSeed() rnd := random.NewSeeded(seed) @@ -264,7 +264,7 @@ func TestReset(t *testing.T) { num2 := rnd.Int() name2 := rnd.NameSize(5, 10) - rnd.Reset(seed) + rnd.Seed(seed) b3 := rnd.Bytes(32) num3 := rnd.Int() From 39fb1de0264e6534e08dcc9dd341b6c9825e070d Mon Sep 17 00:00:00 2001 From: gammazero <11790789+gammazero@users.noreply.github.com> Date: Sun, 12 Jul 2026 13:55:52 -1000 Subject: [PATCH 7/9] replace MakeSeed with StringToSeed and Uint64ToSeed - replace MakeSeed - Updated documentation --- random/doc.go | 7 ++++--- random/random.go | 22 ++++++++++++++++------ random/random_test.go | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 9 deletions(-) diff --git a/random/doc.go b/random/doc.go index 1c1163d..e699c70 100644 --- a/random/doc.go +++ b/random/doc.go @@ -1,6 +1,7 @@ // Package random provides functionality to generate pseudo-random test data. // -// All random numbers and data are created deterministically using a -// pseudo-random number generator. This generator's output is determined by the -// value a seed that us set using the current time, or set explicitly. +// All random numbers and data are created deterministically using the ChaCha8 +// pseudo-random number generator. This generator's output is determined by a +// seed value that can be set explicitly. If not set, then cryptographically +// secure random data is used as the seed. package random diff --git a/random/random.go b/random/random.go index 7330fdc..e280b7c 100644 --- a/random/random.go +++ b/random/random.go @@ -2,6 +2,7 @@ package random import ( crand "crypto/rand" + "crypto/sha256" "encoding/binary" "fmt" "math/rand/v2" @@ -28,16 +29,25 @@ func NewSeed() [32]byte { return seed } -// MakeSeed creates a seed value from a uint64 value. -func MakeSeed(val uint64) [32]byte { +// StringToSeed creates a seed value from a string. The string is hashed so +// that if the string is longer than the necessary seed data, the entire string +// contributes to the seed value. +func StringToSeed(s string) [32]byte { + return sha256.Sum256([]byte(s)) +} + +// Uint64ToSeed creates a seed value from a uint64 value. +func Uint64ToSeed(val uint64) [32]byte { var seed [32]byte binary.BigEndian.PutUint64(seed[:], val) return seed } -// Random contains its own pseudo-random source. The same Random instance must -// not be used concurrently. Create separate Random instances for use in -// concurrent goroutines. +// Random extends math/rand/v2 Rand with functions for generating many useful +// types of data. It also provides a Read function to read random data into a +// buffer and serve as an io.Reader. Random contains its own pseudo-random +// source. The same Random instance must not be used concurrently. Create +// separate Random instances for use in concurrent goroutines. type Random struct { *rand.Rand cc8 *rand.ChaCha8 @@ -443,5 +453,5 @@ func NewRand() *Random { // Deprecated: use NewSeeded(bytesSeed) or NewSeeded(MakeSeed(uint64Seed)) func NewSeededRand(seed uint64) *Random { - return NewSeeded(MakeSeed(seed)) + return NewSeeded(Uint64ToSeed(seed)) } diff --git a/random/random_test.go b/random/random_test.go index 2c976d4..1f61f85 100644 --- a/random/random_test.go +++ b/random/random_test.go @@ -12,6 +12,39 @@ import ( "github.com/stretchr/testify/require" ) +func TestUint64ToSeed(t *testing.T) { + val := random.New().Uint64() + seed := random.Uint64ToSeed(val) + require.Equal(t, seed, random.Uint64ToSeed(val)) + seed2 := random.Uint64ToSeed(val + 1) + require.NotEqual(t, seed, seed2) + + rnd1 := random.NewSeeded(seed) + n1 := rnd1.Int() + + rnd2 := random.NewSeeded(seed) + require.Equal(t, n1, rnd2.Int()) + + rnd3 := random.NewSeeded(seed2) + require.NotEqual(t, n1, rnd3.Int()) +} + +func TestStringToSeed(t *testing.T) { + name := random.New().Name(100) + seed := random.StringToSeed(name) + require.Equal(t, seed, random.StringToSeed(name)) + seed2 := random.StringToSeed(name + "X") + require.NotEqual(t, seed, seed2) + rnd1 := random.NewSeeded(seed) + n1 := rnd1.Int() + + rnd2 := random.NewSeeded(seed) + require.Equal(t, n1, rnd2.Int()) + + rnd3 := random.NewSeeded(seed2) + require.NotEqual(t, n1, rnd3.Int()) +} + func TestAddrs(t *testing.T) { addrs := random.Addrs(3) require.Len(t, addrs, 3) From e9e6d6b057377d453a2dc52381960af3db62a8c7 Mon Sep 17 00:00:00 2001 From: gammazero <11790789+gammazero@users.noreply.github.com> Date: Sun, 12 Jul 2026 14:18:30 -1000 Subject: [PATCH 8/9] update cli tools to take string parameter as random seed --- cli/random-data/README.md | 10 ++-- cli/random-data/main.go | 10 ++-- cli/random-files/README.md | 99 +++++++++++++++++++------------------- cli/random-files/main.go | 7 +-- random/files/files.go | 8 ++- 5 files changed, 63 insertions(+), 71 deletions(-) diff --git a/cli/random-data/README.md b/cli/random-data/README.md index 9c23e00..703d0f4 100644 --- a/cli/random-data/README.md +++ b/cli/random-data/README.md @@ -21,8 +21,8 @@ USAGE OPTIONS: -b64 base-64 encode output - -seed int - random seed, 0 or unset for current time + -seed string + random seed, leave empty to use random value -size int number of bytes to generate ``` @@ -30,8 +30,8 @@ OPTIONS: ## Examples ```sh -random-data -size=64 -b64 -vRujjyEvx8lYiELflaDINvkm5nfueWGCdzEOxhRtz7N2EQjoyrpoMdVVOrwAgNO0tVojDAgu0JpU4hKSsdVl8A== +random-data -size=64 -b64 -seed=hello +ce6rEiwW4jN+E6pCW8UZErXC21sGpkTFuaPEW8VrtayX6nhbbSHOmkFQ1MoHbB2KU2J0ocATYGK4JFEsYNEtlA== ``` -Note: Specifying the same seed will produce the same results. +Note: Specifying the same seed will produce the same results. Specifying no ssed procudes random results. diff --git a/cli/random-data/main.go b/cli/random-data/main.go index 760d0e8..2793889 100644 --- a/cli/random-data/main.go +++ b/cli/random-data/main.go @@ -28,11 +28,11 @@ OPTIONS: var ( b64 bool - seed uint64 + seed string size int64 ) flag.BoolVar(&b64, "b64", false, "base-64 encode output") - flag.Uint64Var(&seed, "seed", 0, "random seed, 0 or unset for current time") + flag.StringVar(&seed, "seed", "", "random seed, leave empty to use random value") flag.Int64Var(&size, "size", 0, "number of bytes to generate") flag.Parse() @@ -53,12 +53,12 @@ OPTIONS: } } -func writeData(seed uint64, size int64, b64 bool) error { +func writeData(seed string, size int64, b64 bool) error { var rnd *random.Random - if seed == 0 { + if seed == "" { rnd = random.New() } else { - rnd = random.NewSeeded(random.MakeSeed(seed)) + rnd = random.NewSeeded(random.StringToSeed(seed)) } var w io.Writer diff --git a/cli/random-files/README.md b/cli/random-files/README.md index fb8dbd8..2ea60f1 100644 --- a/cli/random-files/README.md +++ b/cli/random-files/README.md @@ -34,37 +34,37 @@ OPTIONS: randomize number of files, from 1 to -files -random-size randomize file size, from 1 to -filesize (default true) - -seed int - random seed, 0 for current time + -seed string + random seed, leave empty to use random value ``` ## Examples ```sh > random-files -depth=2 -files=3 -seed=1701 foo -foo/rwd67uvnj9yz- -foo/7vovyvr9 -foo/fjv0w0 -foo/gyubi50rec5/ -foo/gyubi50rec5/vr6x-ce4uupj -foo/gyubi50rec5/ob9ud0e8lt_2e -foo/gyubi50rec5/11gip6zea -foo/nzu5j29-sh-ku4/ -foo/nzu5j29-sh-ku4/vcs1629n -foo/nzu5j29-sh-ku4/rky_i_qsxrp -foo/nzu5j29-sh-ku4/xr1usy5ic0 -foo/w30dzrx2w4_d/ -foo/w30dzrx2w4_d/7ued6 -foo/w30dzrx2w4_d/r1d3j -foo/w30dzrx2w4_d/av7d09i-av -foo/s6ha-58/ -foo/s6ha-58/nukjsxg7t -foo/s6ha-58/7of_84 -foo/s6ha-58/h0jgq8mu1n7u -foo/tq_8/ -foo/tq_8/sx-a2jgmz_mk6 -foo/tq_8/9hzrksz8 -foo/tq_8/8b5swu +foo/mts2ph +foo/wsu46w33-df_v44c +foo/fcuxmunecbs_de +foo/vfpwv_asjhs2nz/ +foo/vfpwv_asjhs2nz/8533 +foo/vfpwv_asjhs2nz/8nj3jouo1xg_ +foo/vfpwv_asjhs2nz/v9awmhr +foo/ujj95a/ +foo/ujj95a/n1mf +foo/ujj95a/-taw_yfnc2 +foo/ujj95a/h5j-x7rkawl6zc8 +foo/v2isuq37wxt/ +foo/v2isuq37wxt/0v1od94eojxv +foo/v2isuq37wxt/u4z-gcvoe4w +foo/v2isuq37wxt/ogm2b-coenbu8t2 +foo/sn35leocdzm15l/ +foo/sn35leocdzm15l/b4w3 +foo/sn35leocdzm15l/f73xrgznl57 +foo/sn35leocdzm15l/7tt51n7lbvv9 +foo/m4ttn58z0x77sbo0/ +foo/m4ttn58z0x77sbo0/ydv5ld_4o7tvw7 +foo/m4ttn58z0x77sbo0/khdlmwfpegefix +foo/m4ttn58z0x77sbo0/ud2mo ``` It made: @@ -72,35 +72,34 @@ It made: ```sh > tree foo foo -├── 7vovyvr9 -├── fjv0w0 -├── gyubi50rec5 -│   ├── 11gip6zea -│   ├── ob9ud0e8lt_2e -│   └── vr6x-ce4uupj -├── nzu5j29-sh-ku4 -│   ├── rky_i_qsxrp -│   ├── vcs1629n -│   └── xr1usy5ic0 -├── rwd67uvnj9yz- -├── s6ha-58 -│   ├── 7of_84 -│   ├── h0jgq8mu1n7u -│   └── nukjsxg7t -├── tq_8 -│   ├── 8b5swu -│   ├── 9hzrksz8 -│   └── sx-a2jgmz_mk6 -└── w30dzrx2w4_d - ├── 7ued6 - ├── av7d09i-av - └── r1d3j +├── fcuxmunecbs_de +├── m4ttn58z0x77sbo0 +│   ├── khdlmwfpegefix +│   ├── ud2mo +│   └── ydv5ld_4o7tvw7 +├── mts2ph +├── sn35leocdzm15l +│   ├── 7tt51n7lbvv9 +│   ├── b4w3 +│   └── f73xrgznl57 +├── ujj95a +│   ├── -taw_yfnc2 +│   ├── h5j-x7rkawl6zc8 +│   └── n1mf +├── v2isuq37wxt +│   ├── 0v1od94eojxv +│   ├── ogm2b-coenbu8t2 +│   └── u4z-gcvoe4w +├── vfpwv_asjhs2nz +│   ├── 8533 +│   ├── 8nj3jouo1xg_ +│   └── v9awmhr +└── wsu46w33-df_v44c 6 directories, 18 files ``` -Note: Specifying the same seed will produce the same results. - +Note: Specifying the same seed will produce the same results. Specifying no ssed procudes random results. ### Acknowledgments diff --git a/cli/random-files/main.go b/cli/random-files/main.go index 3ed77b7..88dcb50 100644 --- a/cli/random-files/main.go +++ b/cli/random-files/main.go @@ -26,7 +26,6 @@ OPTIONS: var ( quiet bool paths []string - seed string ) cfg := files.DefaultConfig() @@ -39,7 +38,7 @@ OPTIONS: flag.BoolVar(&cfg.RandomDirs, "random-dirs", cfg.RandomDirs, "randomize number of subdirectories, from 1 to -dirs") flag.BoolVar(&cfg.RandomFiles, "random-files", cfg.RandomFiles, "randomize number of files, from 1 to -files") flag.BoolVar(&cfg.RandomSize, "random-size", cfg.RandomSize, "randomize file size, from 1 to -filesize") - flag.StringVar(&seed, "seed", "", "random seed, leave empty to use random value") + flag.StringVar(&cfg.Seed, "seed", "", "random seed, leave empty to use random value") flag.Parse() paths = flag.Args() @@ -48,10 +47,6 @@ OPTIONS: cfg.Out = os.Stdout } - if len(seed) != 0 { - cfg.Seed = []byte(seed) - } - err := files.Create(cfg, paths...) if err != nil { fmt.Fprintln(os.Stderr, "error:", err) diff --git a/random/files/files.go b/random/files/files.go index ad275e9..10e6d76 100644 --- a/random/files/files.go +++ b/random/files/files.go @@ -43,7 +43,7 @@ type Config struct { RandomSize bool // Seed sets the seed for the random number generator when set to a // non-empty value. - Seed []byte + Seed string } // DefaultConfig returns default settings for creating random files and @@ -75,12 +75,10 @@ func Create(cfg Config, roots ...string) error { } var rnd *random.Random - if len(cfg.Seed) == 0 { + if cfg.Seed == "" { rnd = random.New() } else { - var seed [32]byte - copy(seed[:], cfg.Seed) - rnd = random.NewSeeded(seed) + rnd = random.NewSeeded(random.StringToSeed(cfg.Seed)) } for _, root := range roots { From 29b7f73d680f180367b73e79655f9593b8a8afd9 Mon Sep 17 00:00:00 2001 From: gammazero <11790789+gammazero@users.noreply.github.com> Date: Sun, 12 Jul 2026 23:01:04 -1000 Subject: [PATCH 9/9] fix error with ensuring unique addresses --- random/random.go | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/random/random.go b/random/random.go index e280b7c..0137e54 100644 --- a/random/random.go +++ b/random/random.go @@ -88,34 +88,35 @@ func (r *Random) Seed(seed [32]byte) { r.cc8.Seed(seed) } -// Addrs returns a slice of n random unique IPv4 addresses. +// Addrs returns a slice of n random unique IPv4 addresses with a random port. +// The string is formatted as a multiaddr: "/ip4/n.n.n.n/tcp/n" func (r *Random) Addrs(n int) []string { - addrs := make([]string, n) + addrs := make([]string, 0, n) addrSet := make(map[string]struct{}, n) - for i := 0; i < n; i++ { - addr := fmt.Sprintf("/ip4/%d.%d.%d.%d/tcp/%d", r.IntN(254)+1, r.IntN(254)+1, r.IntN(254)+1, r.IntN(254)+1, r.IntN(maxTcpPort-minTcpPort)+minTcpPort) + for len(addrs) < n { + addr := fmt.Sprintf("%d.%d.%d.%d", r.IntN(254)+1, r.IntN(254)+1, r.IntN(254)+1, r.IntN(254)+1) if _, ok := addrSet[addr]; ok { - i-- continue } - addrs[i] = addr + addrSet[addr] = struct{}{} + addrs = append(addrs, fmt.Sprintf("/ip4/%s/tcp/%d", addr, r.IntN(maxTcpPort-minTcpPort)+minTcpPort)) } return addrs } -// DnsAddrs returns a slice of n random unique DNS addresses in the format -// "xxxxxxxx.example.com:port". +// DnsAddrs returns a slice of n random unique DNS addresses, formmated as a multiaddr: +// "/dns4/xxxxxxxx.example.com/tcp/n" func (r *Random) DnsAddrs(n int) []string { const nameLen = 8 - addrs := make([]string, n) - addrSet := make(map[string]struct{}, n) - for i := 0; i < n; i++ { - addr := fmt.Sprintf("/dns4/%s.example.com/tcp/%d", r.Name(nameLen), r.IntN(maxTcpPort-minTcpPort)+minTcpPort) - if _, ok := addrSet[addr]; ok { - i-- + addrs := make([]string, 0, n) + nameSet := make(map[string]struct{}, n) + for len(addrs) < n { + name := r.Name(nameLen) + if _, ok := nameSet[name]; ok { continue } - addrs[i] = addr + nameSet[name] = struct{}{} + addrs = append(addrs, fmt.Sprintf("/dns4/%s.example.com/tcp/%d", name, r.IntN(maxTcpPort-minTcpPort)+minTcpPort)) } return addrs }