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 95d4489..2793889 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 string 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.StringVar(&seed, "seed", "", "random seed, leave empty to use random value") 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 - if seed == 0 { - rnd = random.NewRand() +func writeData(seed string, size int64, b64 bool) error { + var rnd *random.Random + if seed == "" { + rnd = random.New() } else { - rnd = random.NewSeededRand(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 0c8ca0a..88dcb50 100644 --- a/cli/random-files/main.go +++ b/cli/random-files/main.go @@ -38,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.Int64Var(&cfg.Seed, "seed", cfg.Seed, "random seed, 0 for current time") + flag.StringVar(&cfg.Seed, "seed", "", "random seed, leave empty to use random value") flag.Parse() paths = flag.Args() 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/files/files.go b/random/files/files.go index e2fc564..10e6d76 100644 --- a/random/files/files.go +++ b/random/files/files.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "io" - "math/rand" "os" "path/filepath" @@ -12,8 +11,8 @@ import ( ) const ( - MinimumNameSize = 4 - fileNameAlpha = "abcdefghijklmnopqrstuvwxyz01234567890-_" + MinimumNameSize = 4 + fileNameAlphabet = "abcdefghijklmnopqrstuvwxyz0123456789-_" ) // Config contains settings for creating random files and directories. @@ -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 string } // DefaultConfig returns default settings for creating random files and @@ -75,11 +74,11 @@ 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 cfg.Seed == "" { + rnd = random.New() } else { - rnd = random.NewSeededRand(cfg.Seed) + rnd = random.NewSeeded(random.StringToSeed(cfg.Seed)) } for _, root := range roots { @@ -128,7 +127,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 +164,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 +181,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 +189,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 +201,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,20 +215,24 @@ 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 - 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 { +func (cfg *Config) writeFile(rnd *random.Random, root string) error { name := cfg.randomName(rnd) filePath := filepath.Join(root, name) f, err := os.Create(filePath) @@ -240,7 +243,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 2e95633..0137e54 100644 --- a/random/random.go +++ b/random/random.go @@ -1,10 +1,11 @@ package random import ( + crand "crypto/rand" + "crypto/sha256" + "encoding/binary" "fmt" - "math/rand" - "sync/atomic" - "time" + "math/rand/v2" blocks "github.com/ipfs/go-block-format" "github.com/ipfs/go-cid" @@ -15,113 +16,133 @@ 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()) +// 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 } -// 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)) +// 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)) } -// 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)) +// 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 } -// Returns the initial seed used for the pseudo-random number generator, or the -// most recent value set by SetSeed. -func Seed() int64 { - return initSeed +// 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 } -// 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()) +// New returns a new Random instance that contains its own pseudo-random number +// source, seeded with a random value. +func New() *Random { + var seed [32]byte + _, _ = crand.Read(seed[:]) + cc8 := rand.NewChaCha8(seed) + + return &Random{ + cc8: cc8, + Rand: rand.New(cc8), + } } -// Addrs returns a slice of n random unique IPv4 addresses. -func Addrs(n int) []string { - addrs := make([]string, n) +// NewSeeded returns a new Random instance that contains its own pseudo-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(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) +} + +// Seed resets the Random instance to behave the same way as NewSeeded(seed). +func (r *Random) Seed(seed [32]byte) { + r.cc8.Seed(seed) +} + +// 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, 0, 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) + 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". -func DnsAddrs(n int) []string { - const ( - nameLen = 8 - lowerAsciiA = 97 - ) - 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) - if _, ok := addrSet[addr]; ok { - i-- +// 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, 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 } // 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 int64) []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 int64) []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 +153,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 +165,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) +// 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)) } -// DnsMultiaddrs returns a slice of n random unique Multiaddrs with DNS addresses. -func DnsMultiaddrs(n int) []multiaddr.Multiaddr { - return multiaddrs(n, DnsAddrs) -} - -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 +202,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,8 +248,34 @@ func Peers(n int) []peer.ID { return peerIDs } -func addrInfos(numPeers, numAddrs int, multiaddrsFunc func(int) []multiaddr.Multiaddr) []peer.AddrInfo { - peerIDs := Peers(numPeers) +// 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) +} + +// 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) +} + +// 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) +} + +// 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) +} + +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{ @@ -233,56 +286,173 @@ func addrInfos(numPeers, numAddrs int, multiaddrsFunc func(int) []multiaddr.Mult return addrInfos } -// 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. +// 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 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 int64) []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 addrInfos(numPeers, numAddrs, Multiaddrs) + return New().AddrInfos(numPeers, numAddrs) } -// 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. +// 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 addrInfos(numPeers, numAddrs, DnsMultiaddrs) + return New().DnsAddrInfos(numPeers, numAddrs) } -// 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. +// 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 addrInfos(numPeers, numAddrs, HttpMultiaddrs) + return New().HttpAddrInfos(numPeers, numAddrs) } -// 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. +// 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 addrInfos(numPeers, numAddrs, HttpDnsMultiaddrs) + return New().HttpDnsAddrInfos(numPeers, numAddrs) } -// 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)} +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 } - seq := make([]uint64, n) - seqVal := globalSeqGen.Add(uint64(n)) - uint64(n-1) - for i := range n { - seq[i] = seqVal + uint64(i) + 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 seq + return maddrs +} + +// Deprecated: use New() +func NewRand() *Random { + return New() } -// SequenceNext returns the next unique global sequence value. This is -// equivalent to Sequence(1)[0]. -func SequenceNext() uint64 { - return globalSeqGen.Add(1) +// Deprecated: use NewSeeded(bytesSeed) or NewSeeded(MakeSeed(uint64Seed)) +func NewSeededRand(seed uint64) *Random { + return NewSeeded(Uint64ToSeed(seed)) } diff --git a/random/random_test.go b/random/random_test.go index 208b72f..1f61f85 100644 --- a/random/random_test.go +++ b/random/random_test.go @@ -2,16 +2,61 @@ package random_test import ( "strings" - "sync" "testing" 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 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) + 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 +76,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 +116,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 +195,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 +233,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) @@ -119,99 +285,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() + 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.Seed(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 - }() - } - - ready.Wait() - close(startGate) - - 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) + b3 := rnd.Bytes(32) + num3 := rnd.Int() + name3 := rnd.NameSize(5, 10) + + require.NotEqual(t, b1, b2) + require.NotEqual(t, num1, num2) + require.NotEqual(t, name1, name2) + + 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) + } } 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) +}