From cbf0cf0534568e02e61daa2e93ebd87572b8f3bb Mon Sep 17 00:00:00 2001 From: FTHans Date: Mon, 2 Jun 2025 19:08:43 +0200 Subject: [PATCH 1/9] added DeterministicRandom for random but auditable results --- cmd/simulator/main.go | 224 +++++++++++++++++++++++++++++++----------- random.go | 67 +++++++++++++ random_test.go | 47 +++++++++ 3 files changed, 278 insertions(+), 60 deletions(-) diff --git a/cmd/simulator/main.go b/cmd/simulator/main.go index 483c22e..81da5b6 100644 --- a/cmd/simulator/main.go +++ b/cmd/simulator/main.go @@ -17,6 +17,7 @@ func main() { menu.AddItem("Random Uniform Float64 with range (0,1]", "UniformFloat64") menu.AddItem("Random Uniform Int with range (min, max)", "UniformInt64") + menu.AddItem("Deterministic Random with seed and probabilities", "DeterministicRandom") menu.AddItem("Exit", "Exit") choice := menu.Display() @@ -33,24 +34,26 @@ func main() { res, errPrompt := stringPrompt("how many results do you want to generate?") if errPrompt != nil { fmt.Println("error getting results from prompt:", errPrompt) - } else { - numbersToGenerate, errParseInt := strconv.ParseInt(res, 10, 32) - if errParseInt != nil { - fmt.Println(res, "is an invalid number, try again") - } else { - fmt.Print("generating ", res, " random numbers... ") - - fileName, errGenerate := generateUniformFloat64(int32(numbersToGenerate)) - if errGenerate != nil { - fmt.Print("error: ", errGenerate) - return - } - - fmt.Println("file with results was generated:", fileName) - - break - } + continue + } + + numbersToGenerate, errParseInt := strconv.Atoi(res) + if errParseInt != nil { + fmt.Println(res, "is an invalid number, try again") + continue + } + + fmt.Print("generating ", res, " random numbers... ") + + fileName, errGenerate := generateUniformFloat64(numbersToGenerate) + if errGenerate != nil { + fmt.Print("error: ", errGenerate) + return } + + fmt.Println("file with results was generated:", fileName) + + break } case "UniformInt64": @@ -58,47 +61,113 @@ func main() { res, errPrompt := stringPrompt("how many results do you want to generate?") if errPrompt != nil { fmt.Println("error getting results from prompt:", errPrompt) - } else { - numbersToGenerate, errParseIntCount := strconv.ParseInt(res, 10, 32) - if errParseIntCount != nil { - fmt.Println(res, "is an invalid number, try again") - } else { - res, errPrompt = stringPrompt("whats the minimum number?") - if errPrompt != nil { - fmt.Println("error getting results from prompt:", errPrompt) - } else { - minimumNumber, errParseIntMin := strconv.ParseInt(res, 10, 32) - if errParseIntMin != nil { - fmt.Println(res, "is an invalid number, try again") - } else { - res, errPrompt = stringPrompt("whats the maximum number?") - if errPrompt != nil { - fmt.Println("error getting results from prompt:", errPrompt) - } else { - maximumNumber, errParseIntMax := strconv.ParseInt(res, 10, 32) - if errParseIntMax != nil { - fmt.Println(res, "is an invalid number, try again") - } else if maximumNumber <= minimumNumber { - fmt.Println("maximum cannot be less than or equal to minimum number, try again") - } else { - - fmt.Print("generating ", numbersToGenerate, " random numbers between ", minimumNumber, " and ", maximumNumber, "... ") - - fileName, errGenerate := generateUniformInt64(int32(numbersToGenerate), int32(minimumNumber), int32(maximumNumber)) - if errGenerate != nil { - fmt.Print("error: ", errGenerate) - return - } - - fmt.Println("file with results was generated:", fileName) - - break - } - } - } - } + continue + } + + numbersToGenerate, errParseIntCount := strconv.Atoi(res) + if errParseIntCount != nil { + fmt.Println(res, "is an invalid number, try again") + continue + } + + res, errPrompt = stringPrompt("whats the minimum number?") + if errPrompt != nil { + fmt.Println("error getting results from prompt:", errPrompt) + continue + } + + minimumNumber, errParseIntMin := strconv.ParseInt(res, 10, 32) + if errParseIntMin != nil { + fmt.Println(res, "is an invalid number, try again") + continue + } + + res, errPrompt = stringPrompt("whats the maximum number?") + if errPrompt != nil { + fmt.Println("error getting results from prompt:", errPrompt) + continue + } + + maximumNumber, errParseIntMax := strconv.ParseInt(res, 10, 32) + if errParseIntMax != nil { + fmt.Println(res, "is an invalid number, try again") + continue + } else if maximumNumber <= minimumNumber { + fmt.Println("maximum cannot be less than or equal to minimum number, try again") + continue + } + + fmt.Print("generating ", numbersToGenerate, " random numbers between ", minimumNumber, " and ", maximumNumber, "... ") + + fileName, errGenerate := generateUniformInt64(numbersToGenerate, int32(minimumNumber), int32(maximumNumber)) + if errGenerate != nil { + fmt.Print("error: ", errGenerate) + return + } + + fmt.Println("file with results was generated:", fileName) + + break + } + + case "DeterministicRandom": + for { + res, errPrompt := stringPrompt("how many results do you want to generate?") + if errPrompt != nil { + fmt.Println("error getting results from prompt:", errPrompt) + continue + } + + numbersToGenerate, errParseIntCount := strconv.Atoi(res) + if errParseIntCount != nil { + fmt.Println(res, "is an invalid number, try again") + continue + } + + res, errPrompt = stringPrompt("what seed should be used (i.e. 9912f3bcf715a55ae5c9d47f9f6562599912f3bcf715a55ae5c9d47f9f656259)?") + if errPrompt != nil { + fmt.Println("error getting results from prompt:", errPrompt) + continue + } + + seedHex := res + if len(seedHex) != 64 { + fmt.Println("the seed needs to be 64 characters [a-f0-9], try again") + continue + } + + res, errPrompt = stringPrompt("what probabilities should be used (i.e. 0.3, 0.5, 0.2)?") + if errPrompt != nil { + fmt.Println("error getting results from prompt:", errPrompt) + continue + } + + probabilitiesAsStrings := strings.Split(res, ",") + if len(probabilitiesAsStrings) == 0 { + fmt.Println("no probabilities set, try again") + continue + } + + probabilities := make([]float64, len(probabilitiesAsStrings)) + for i, v := range probabilitiesAsStrings { + probabilities[i], err = strconv.ParseFloat(strings.TrimSpace(v), 64) + if err != nil { + fmt.Println("invalid probability, try again:", err) + continue } } + + fmt.Print("generating ", numbersToGenerate, " random numbers... ") + + fileName, errGenerate := generateDeterministicRandom(numbersToGenerate, seedHex, probabilities) + if errGenerate != nil { + fmt.Print("error: ", errGenerate) + return + } + + fmt.Println("file with results was generated:", fileName) + + break } case "Exit": @@ -127,7 +196,7 @@ func stringPrompt(label string) (string, error) { return strings.TrimSpace(s), nil } -func generateUniformFloat64(numbersToGenerate int32) (string, error) { +func generateUniformFloat64(numbersToGenerate int) (string, error) { fileName := fmt.Sprintf("cmd/simulator/results/UniformFloat64-%v.csv", time.Now().UnixMilli()) f, err := os.Create(filepath.Clean(fileName)) @@ -142,7 +211,7 @@ func generateUniformFloat64(numbersToGenerate int32) (string, error) { return "", errWriteString } - for i := int32(0); i < numbersToGenerate; i++ { + for i := 0; i < numbersToGenerate; i++ { rnd, errRnr := random.UniformFloat64() if errRnr != nil { return "", errRnr @@ -157,7 +226,7 @@ func generateUniformFloat64(numbersToGenerate int32) (string, error) { return fileName, nil } -func generateUniformInt64(numbersToGenerate int32, min int32, max int32) (string, error) { +func generateUniformInt64(numbersToGenerate int, min int32, max int32) (string, error) { fileName := fmt.Sprintf("cmd/simulator/results/UniformInt64-%v.csv", time.Now().UnixMilli()) f, err := os.Create(filepath.Clean(fileName)) @@ -172,7 +241,7 @@ func generateUniformInt64(numbersToGenerate int32, min int32, max int32) (string return "", errWriteString } - for i := int32(0); i < numbersToGenerate; i++ { + for i := 0; i < numbersToGenerate; i++ { rnd, errRnr := random.UniformInt64(min, max) if errRnr != nil { return "", errRnr @@ -186,3 +255,38 @@ func generateUniformInt64(numbersToGenerate int32, min int32, max int32) (string return fileName, nil } + +func generateDeterministicRandom(numbersToGenerate int, seed string, probabilities []float64) (string, error) { + fileName := fmt.Sprintf("cmd/simulator/results/DeterministicRandom-%v.csv", time.Now().UnixMilli()) + + f, err := os.Create(filepath.Clean(fileName)) + if err != nil { + return "", err + } + + defer f.Close() + + _, errWriteString := f.WriteString(fmt.Sprintf("DeterministicRandom (%v %v)\n", seed, probabilities)) + if errWriteString != nil { + return "", errWriteString + } + + _, errWriteString = f.WriteString(fmt.Sprintf("SequenceNr, SelectedIndex\n")) + if errWriteString != nil { + return "", errWriteString + } + + for i := 0; i < numbersToGenerate; i++ { + rnd, errRnr := random.DeterministicRandom(seed, i, probabilities) + if errRnr != nil { + return "", errRnr + } + + _, errWriteString = f.WriteString(fmt.Sprintf("%v, %v\n", i, rnd)) + if errWriteString != nil { + return "", errWriteString + } + } + + return fileName, nil +} diff --git a/random.go b/random.go index f3b21d7..1b5fd31 100644 --- a/random.go +++ b/random.go @@ -2,7 +2,9 @@ package random import ( "crypto/rand" + "crypto/sha256" "encoding/binary" + "encoding/hex" "errors" "fmt" "math" @@ -54,3 +56,68 @@ func Truncate(val float64, precision int) float64 { multiplier := math.Pow(10, float64(precision)) return math.Floor(val*multiplier) / multiplier } + +func DeterministicRandom(seedHex string, sequenceNr int, probabilities []float64) (int, error) { + // Validate input + if len(seedHex) != 64 { + return 0, errors.New("seedHex must be 64 bytes") + } else if sequenceNr < 0 { + return 0, errors.New("sequenceNr must be larger than than or equal to 0") + } else if len(probabilities) == 0 { + return 0, errors.New("probabilities must not be empty") + } + + // Decode the seed + seed, err := hex.DecodeString(seedHex) + if err != nil { + return 0, fmt.Errorf("invalid seed hex: %w", err) + } else if len(seed) != 32 { + return 0, errors.New("seed must decode to exactly 32 bytes") + } + + // Validate and sum probabilities + sum := 0.0 + for _, p := range probabilities { + if p < 0 || p > 1 { + return 0, fmt.Errorf("invalid probability %f", p) + } + sum += p + } + + const epsilon = 1e-12 + if math.Abs(sum-1.0) > epsilon { + return 0, fmt.Errorf("sum of probabilities = %f; must be exactly 1.0", sum) + } + + // Build cumulative thresholds + thresholds := make([]uint64, len(probabilities)) + cumulative := 0.0 + for i, p := range probabilities { + cumulative += p + if i == len(probabilities)-1 { + thresholds[i] = math.MaxUint64 // ensure full coverage + } else { + thresholds[i] = uint64(cumulative * math.Pow(2, 64)) + } + } + + // Compute R(sequence) + var buf [8]byte + binary.BigEndian.PutUint64(buf[:], uint64(sequenceNr)) + + h := sha256.New() + h.Write(seed) + h.Write(buf[:]) + hash := h.Sum(nil) + x := binary.BigEndian.Uint64(hash[:8]) + + // Find the prize index + for i, t := range thresholds { + if x < t { + return i, nil + } + } + + // Should never happen if sum == 1.0 + return 0, errors.New("unexpected: no prize selected despite sum == 1.0") +} diff --git a/random_test.go b/random_test.go index 2495267..ec5f920 100644 --- a/random_test.go +++ b/random_test.go @@ -59,3 +59,50 @@ func Test_Truncate(t *testing.T) { number = Truncate(0.1234567891, 9) assert.Equal(t, 0.123456789, number) } + +func Test_DeterministicRandom(t *testing.T) { + testCases := []struct { + seedHex string + sequence int + probabilities []float64 + expectedIndex int + }{ + { + seedHex: "9912f3bcf715a55ae5c9d47f9f6562599912f3bcf715a55ae5c9d47f9f656259", + sequence: 0, + probabilities: []float64{0.2, 0.2, 0.2, 0.2, 0.2}, + expectedIndex: 0, + }, + { + seedHex: "9912f3bcf715a55ae5c9d47f9f6562599912f3bcf715a55ae5c9d47f9f656259", + sequence: 1, + probabilities: []float64{0.2, 0.2, 0.2, 0.2, 0.2}, + expectedIndex: 2, + }, + { + seedHex: "9912f3bcf715a55ae5c9d47f9f6562599912f3bcf715a55ae5c9d47f9f656259", + sequence: 2, + probabilities: []float64{0.2, 0.2, 0.2, 0.2, 0.2}, + expectedIndex: 1, + }, + { + seedHex: "0000000000000000000000000000000000000000000000000000000000000000", + sequence: 0, + probabilities: []float64{0.1, 0.9}, + expectedIndex: 1, + }, + + { + seedHex: "9912f3bcf715a55ae5c9d47f9f6562599912f3bcf715a55ae5c9d47f9f656259", + sequence: 9, + probabilities: []float64{0.3, 0.5, 0.2}, + expectedIndex: 2, + }, + } + + for _, testCase := range testCases { + selectedIndex, err := DeterministicRandom(testCase.seedHex, testCase.sequence, testCase.probabilities) + assert.Nil(t, err) + assert.Equal(t, testCase.expectedIndex, selectedIndex) + } +} From c39b223c5407ceab09b65a9d75a8a8f21b80b77f Mon Sep 17 00:00:00 2001 From: FTHans Date: Tue, 3 Jun 2025 10:34:19 +0200 Subject: [PATCH 2/9] added http/grpc support for DeterministicRandom --- cmd/grpc/main.go | 20 +++++- cmd/http/main.go | 63 ++++++++++++++++++ pkg/pb/service.pb.go | 130 ++++++++++++++++++++++++++++++++++---- pkg/pb/service.proto | 10 +++ pkg/pb/service_grpc.pb.go | 42 +++++++++++- random.go | 20 +++--- 6 files changed, 259 insertions(+), 26 deletions(-) diff --git a/cmd/grpc/main.go b/cmd/grpc/main.go index 20ade55..615bafa 100644 --- a/cmd/grpc/main.go +++ b/cmd/grpc/main.go @@ -37,7 +37,7 @@ func main() { reflection.Register(s) - randomServer := NewRandomGRPCServer() + randomServer := NewRandomGRPCServer(*config.SEEDHEX) pb.RegisterRandomServer(s, randomServer) lis, errListen := net.Listen("tcp", fmt.Sprintf(":%v", *config.GRPCPort)) @@ -57,10 +57,13 @@ func main() { type RandomGRPCServer struct { pb.UnimplementedRandomServer + seed string } -func NewRandomGRPCServer() *RandomGRPCServer { - return &RandomGRPCServer{} +func NewRandomGRPCServer(seed string) *RandomGRPCServer { + return &RandomGRPCServer{ + seed: seed, + } } func (rs *RandomGRPCServer) GetRandomInt64(ctx context.Context, req *pb.GetRandomInt64Request) (*pb.GetRandomInt64Response, error) { @@ -88,3 +91,14 @@ func (rs *RandomGRPCServer) GetRandomFloat64(ctx context.Context, req *pb.GetRan Number: number, }, nil } + +func (rs *RandomGRPCServer) GetDeterministicRandom(ctx context.Context, req *pb.GetDeterministicRandomRequest) (*pb.GetDeterministicRandomResponse, error) { + number, err := random.DeterministicRandom(rs.seed, int(req.Sequence), req.Probabilities) + if err != nil { + return nil, err + } + + return &pb.GetDeterministicRandomResponse{ + Number: int32(number), + }, nil +} diff --git a/cmd/http/main.go b/cmd/http/main.go index 6aed326..5f827aa 100644 --- a/cmd/http/main.go +++ b/cmd/http/main.go @@ -9,6 +9,7 @@ import ( "math" "net/http" "strconv" + "strings" "time" ) @@ -94,6 +95,68 @@ func main() { c.String(http.StatusOK, fmt.Sprintf("%v", number)) }) + ginEngine.GET("/getDeterministicRandom", func(c *gin.Context) { + seed := *config.SEEDHEX + + sequence := 0 + sequenceAsStr := c.Query("s") + if len(sequenceAsStr) == 0 { + c.String(http.StatusBadRequest, "sequence is missing") + c.Abort() + return + } else { + sequenceAsNumber, errAtoi := strconv.Atoi(sequenceAsStr) + if errAtoi != nil { + c.String(http.StatusBadRequest, "unable to parse sequence as number") + c.Abort() + return + } else if sequenceAsNumber < 0 || sequenceAsNumber >= math.MaxInt32 { + c.String(http.StatusBadRequest, "sequence must be between 0 and 2,147,483,647") + c.Abort() + return + } else { + sequence = sequenceAsNumber + } + } + + var probabilities []float64 + probabilitiesAsStr := c.Query("p") + if len(probabilitiesAsStr) == 0 { + c.String(http.StatusBadRequest, "probabilities are missing") + c.Abort() + return + } else if len(probabilitiesAsStr) > 300 { + c.String(http.StatusBadRequest, "string of probabilities must be less than 300 characters") + c.Abort() + return + } else { + probabilitiesAsStrList := strings.Split(probabilitiesAsStr, ",") + if len(probabilitiesAsStrList) == 0 { + c.String(http.StatusBadRequest, "invalid probabilities, use comma separated list (i.e. 0.01,0.09,0.9") + c.Abort() + return + } + + for _, v := range probabilitiesAsStrList { + probability, errParse := strconv.ParseFloat(strings.TrimSpace(v), 64) + if errParse != nil { + c.String(http.StatusBadRequest, fmt.Sprintf("invalid probability: %s", strings.TrimSpace(v))) + c.Abort() + return + } + probabilities = append(probabilities, probability) + } + } + + number, errDeterministicRandom := random.DeterministicRandom(seed, sequence, probabilities) + if errDeterministicRandom != nil { + c.String(http.StatusBadRequest, errDeterministicRandom.Error()) + c.Abort() + return + } + c.String(http.StatusOK, fmt.Sprintf("%v", number)) + }) + // start server slog.Info(fmt.Sprintf("http server listening on %v", *config.HTTPPort)) errRun := ginEngine.Run(fmt.Sprintf(":%v", *config.HTTPPort)) diff --git a/pkg/pb/service.pb.go b/pkg/pb/service.pb.go index 063cfa8..101bd8e 100644 --- a/pkg/pb/service.pb.go +++ b/pkg/pb/service.pb.go @@ -197,6 +197,102 @@ func (x *GetRandomInt64Response) GetNumber() int64 { return 0 } +type GetDeterministicRandomRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Sequence int32 `protobuf:"varint,1,opt,name=sequence,proto3" json:"sequence,omitempty"` + Probabilities []float64 `protobuf:"fixed64,2,rep,packed,name=probabilities,proto3" json:"probabilities,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetDeterministicRandomRequest) Reset() { + *x = GetDeterministicRandomRequest{} + mi := &file_pkg_pb_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetDeterministicRandomRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetDeterministicRandomRequest) ProtoMessage() {} + +func (x *GetDeterministicRandomRequest) ProtoReflect() protoreflect.Message { + mi := &file_pkg_pb_service_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetDeterministicRandomRequest.ProtoReflect.Descriptor instead. +func (*GetDeterministicRandomRequest) Descriptor() ([]byte, []int) { + return file_pkg_pb_service_proto_rawDescGZIP(), []int{4} +} + +func (x *GetDeterministicRandomRequest) GetSequence() int32 { + if x != nil { + return x.Sequence + } + return 0 +} + +func (x *GetDeterministicRandomRequest) GetProbabilities() []float64 { + if x != nil { + return x.Probabilities + } + return nil +} + +type GetDeterministicRandomResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Number int32 `protobuf:"varint,1,opt,name=number,proto3" json:"number,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetDeterministicRandomResponse) Reset() { + *x = GetDeterministicRandomResponse{} + mi := &file_pkg_pb_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetDeterministicRandomResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetDeterministicRandomResponse) ProtoMessage() {} + +func (x *GetDeterministicRandomResponse) ProtoReflect() protoreflect.Message { + mi := &file_pkg_pb_service_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetDeterministicRandomResponse.ProtoReflect.Descriptor instead. +func (*GetDeterministicRandomResponse) Descriptor() ([]byte, []int) { + return file_pkg_pb_service_proto_rawDescGZIP(), []int{5} +} + +func (x *GetDeterministicRandomResponse) GetNumber() int32 { + if x != nil { + return x.Number + } + return 0 +} + var File_pkg_pb_service_proto protoreflect.FileDescriptor const file_pkg_pb_service_proto_rawDesc = "" + @@ -209,10 +305,16 @@ const file_pkg_pb_service_proto_rawDesc = "" + "\x03min\x18\x01 \x01(\x05R\x03min\x12\x10\n" + "\x03max\x18\x02 \x01(\x05R\x03max\"0\n" + "\x16GetRandomInt64Response\x12\x16\n" + - "\x06number\x18\x01 \x01(\x03R\x06number2\xb0\x01\n" + + "\x06number\x18\x01 \x01(\x03R\x06number\"a\n" + + "\x1dGetDeterministicRandomRequest\x12\x1a\n" + + "\bsequence\x18\x01 \x01(\x05R\bsequence\x12$\n" + + "\rprobabilities\x18\x02 \x03(\x01R\rprobabilities\"8\n" + + "\x1eGetDeterministicRandomResponse\x12\x16\n" + + "\x06number\x18\x01 \x01(\x05R\x06number2\x99\x02\n" + "\x06Random\x12U\n" + "\x10GetRandomFloat64\x12\x1f.random.GetRandomFloat64Request\x1a .random.GetRandomFloat64Response\x12O\n" + - "\x0eGetRandomInt64\x12\x1d.random.GetRandomInt64Request\x1a\x1e.random.GetRandomInt64ResponseB\x80\x01\n" + + "\x0eGetRandomInt64\x12\x1d.random.GetRandomInt64Request\x1a\x1e.random.GetRandomInt64Response\x12g\n" + + "\x16GetDeterministicRandom\x12%.random.GetDeterministicRandomRequest\x1a&.random.GetDeterministicRandomResponseB\x80\x01\n" + "\n" + "com.randomB\fServiceProtoP\x01Z,github.com/fasttrack-solutions/random/pkg/pb\xa2\x02\x03RXX\xaa\x02\x06Random\xca\x02\x06Random\xe2\x02\x12Random\\GPBMetadata\xea\x02\x06Randomb\x06proto3" @@ -228,20 +330,24 @@ func file_pkg_pb_service_proto_rawDescGZIP() []byte { return file_pkg_pb_service_proto_rawDescData } -var file_pkg_pb_service_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_pkg_pb_service_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_pkg_pb_service_proto_goTypes = []any{ - (*GetRandomFloat64Request)(nil), // 0: random.GetRandomFloat64Request - (*GetRandomFloat64Response)(nil), // 1: random.GetRandomFloat64Response - (*GetRandomInt64Request)(nil), // 2: random.GetRandomInt64Request - (*GetRandomInt64Response)(nil), // 3: random.GetRandomInt64Response + (*GetRandomFloat64Request)(nil), // 0: random.GetRandomFloat64Request + (*GetRandomFloat64Response)(nil), // 1: random.GetRandomFloat64Response + (*GetRandomInt64Request)(nil), // 2: random.GetRandomInt64Request + (*GetRandomInt64Response)(nil), // 3: random.GetRandomInt64Response + (*GetDeterministicRandomRequest)(nil), // 4: random.GetDeterministicRandomRequest + (*GetDeterministicRandomResponse)(nil), // 5: random.GetDeterministicRandomResponse } var file_pkg_pb_service_proto_depIdxs = []int32{ 0, // 0: random.Random.GetRandomFloat64:input_type -> random.GetRandomFloat64Request 2, // 1: random.Random.GetRandomInt64:input_type -> random.GetRandomInt64Request - 1, // 2: random.Random.GetRandomFloat64:output_type -> random.GetRandomFloat64Response - 3, // 3: random.Random.GetRandomInt64:output_type -> random.GetRandomInt64Response - 2, // [2:4] is the sub-list for method output_type - 0, // [0:2] is the sub-list for method input_type + 4, // 2: random.Random.GetDeterministicRandom:input_type -> random.GetDeterministicRandomRequest + 1, // 3: random.Random.GetRandomFloat64:output_type -> random.GetRandomFloat64Response + 3, // 4: random.Random.GetRandomInt64:output_type -> random.GetRandomInt64Response + 5, // 5: random.Random.GetDeterministicRandom:output_type -> random.GetDeterministicRandomResponse + 3, // [3:6] is the sub-list for method output_type + 0, // [0:3] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name @@ -258,7 +364,7 @@ func file_pkg_pb_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_pkg_pb_service_proto_rawDesc), len(file_pkg_pb_service_proto_rawDesc)), NumEnums: 0, - NumMessages: 4, + NumMessages: 6, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/pb/service.proto b/pkg/pb/service.proto index 87f7d92..cd64c07 100644 --- a/pkg/pb/service.proto +++ b/pkg/pb/service.proto @@ -8,6 +8,7 @@ option go_package = "github.com/fasttrack-solutions/random/pkg/pb"; service Random { rpc GetRandomFloat64(GetRandomFloat64Request) returns (GetRandomFloat64Response); rpc GetRandomInt64(GetRandomInt64Request) returns (GetRandomInt64Response); + rpc GetDeterministicRandom(GetDeterministicRandomRequest) returns (GetDeterministicRandomResponse); } message GetRandomFloat64Request {} @@ -24,3 +25,12 @@ message GetRandomInt64Request { message GetRandomInt64Response { int64 number = 1; } + +message GetDeterministicRandomRequest { + int32 sequence = 1; + repeated double probabilities = 2; +} + +message GetDeterministicRandomResponse { + int32 number = 1; +} diff --git a/pkg/pb/service_grpc.pb.go b/pkg/pb/service_grpc.pb.go index 69d1228..0074fe3 100644 --- a/pkg/pb/service_grpc.pb.go +++ b/pkg/pb/service_grpc.pb.go @@ -19,8 +19,9 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - Random_GetRandomFloat64_FullMethodName = "/random.Random/GetRandomFloat64" - Random_GetRandomInt64_FullMethodName = "/random.Random/GetRandomInt64" + Random_GetRandomFloat64_FullMethodName = "/random.Random/GetRandomFloat64" + Random_GetRandomInt64_FullMethodName = "/random.Random/GetRandomInt64" + Random_GetDeterministicRandom_FullMethodName = "/random.Random/GetDeterministicRandom" ) // RandomClient is the client API for Random service. @@ -29,6 +30,7 @@ const ( type RandomClient interface { GetRandomFloat64(ctx context.Context, in *GetRandomFloat64Request, opts ...grpc.CallOption) (*GetRandomFloat64Response, error) GetRandomInt64(ctx context.Context, in *GetRandomInt64Request, opts ...grpc.CallOption) (*GetRandomInt64Response, error) + GetDeterministicRandom(ctx context.Context, in *GetDeterministicRandomRequest, opts ...grpc.CallOption) (*GetDeterministicRandomResponse, error) } type randomClient struct { @@ -59,12 +61,23 @@ func (c *randomClient) GetRandomInt64(ctx context.Context, in *GetRandomInt64Req return out, nil } +func (c *randomClient) GetDeterministicRandom(ctx context.Context, in *GetDeterministicRandomRequest, opts ...grpc.CallOption) (*GetDeterministicRandomResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetDeterministicRandomResponse) + err := c.cc.Invoke(ctx, Random_GetDeterministicRandom_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // RandomServer is the server API for Random service. // All implementations should embed UnimplementedRandomServer // for forward compatibility. type RandomServer interface { GetRandomFloat64(context.Context, *GetRandomFloat64Request) (*GetRandomFloat64Response, error) GetRandomInt64(context.Context, *GetRandomInt64Request) (*GetRandomInt64Response, error) + GetDeterministicRandom(context.Context, *GetDeterministicRandomRequest) (*GetDeterministicRandomResponse, error) } // UnimplementedRandomServer should be embedded to have @@ -80,6 +93,9 @@ func (UnimplementedRandomServer) GetRandomFloat64(context.Context, *GetRandomFlo func (UnimplementedRandomServer) GetRandomInt64(context.Context, *GetRandomInt64Request) (*GetRandomInt64Response, error) { return nil, status.Errorf(codes.Unimplemented, "method GetRandomInt64 not implemented") } +func (UnimplementedRandomServer) GetDeterministicRandom(context.Context, *GetDeterministicRandomRequest) (*GetDeterministicRandomResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetDeterministicRandom not implemented") +} func (UnimplementedRandomServer) testEmbeddedByValue() {} // UnsafeRandomServer may be embedded to opt out of forward compatibility for this service. @@ -136,6 +152,24 @@ func _Random_GetRandomInt64_Handler(srv interface{}, ctx context.Context, dec fu return interceptor(ctx, in, info, handler) } +func _Random_GetDeterministicRandom_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetDeterministicRandomRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RandomServer).GetDeterministicRandom(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Random_GetDeterministicRandom_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RandomServer).GetDeterministicRandom(ctx, req.(*GetDeterministicRandomRequest)) + } + return interceptor(ctx, in, info, handler) +} + // Random_ServiceDesc is the grpc.ServiceDesc for Random service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -151,6 +185,10 @@ var Random_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetRandomInt64", Handler: _Random_GetRandomInt64_Handler, }, + { + MethodName: "GetDeterministicRandom", + Handler: _Random_GetDeterministicRandom_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "pkg/pb/service.proto", diff --git a/random.go b/random.go index 1b5fd31..0061907 100644 --- a/random.go +++ b/random.go @@ -57,12 +57,14 @@ func Truncate(val float64, precision int) float64 { return math.Floor(val*multiplier) / multiplier } -func DeterministicRandom(seedHex string, sequenceNr int, probabilities []float64) (int, error) { +// DeterministicRandom creates deterministic random numbers using a seed. +// The same seed, sequence number and probabilities generate the same outcome. +func DeterministicRandom(seedHex string, sequence int, probabilities []float64) (int, error) { // Validate input if len(seedHex) != 64 { return 0, errors.New("seedHex must be 64 bytes") - } else if sequenceNr < 0 { - return 0, errors.New("sequenceNr must be larger than than or equal to 0") + } else if sequence < 0 { + return 0, errors.New("sequence must be larger than than or equal to 0") } else if len(probabilities) == 0 { return 0, errors.New("probabilities must not be empty") } @@ -79,14 +81,14 @@ func DeterministicRandom(seedHex string, sequenceNr int, probabilities []float64 sum := 0.0 for _, p := range probabilities { if p < 0 || p > 1 { - return 0, fmt.Errorf("invalid probability %f", p) + return 0, fmt.Errorf("invalid input %v; valid range 0 <= p <= 1", p) } sum += p } - const epsilon = 1e-12 + const epsilon = 1e-12 // allow for minor float faults if math.Abs(sum-1.0) > epsilon { - return 0, fmt.Errorf("sum of probabilities = %f; must be exactly 1.0", sum) + return 0, fmt.Errorf("sum of probabilities %v; must be exactly 1.0", sum) } // Build cumulative thresholds @@ -101,9 +103,9 @@ func DeterministicRandom(seedHex string, sequenceNr int, probabilities []float64 } } - // Compute R(sequence) + // Compute random number var buf [8]byte - binary.BigEndian.PutUint64(buf[:], uint64(sequenceNr)) + binary.BigEndian.PutUint64(buf[:], uint64(sequence)) h := sha256.New() h.Write(seed) @@ -111,7 +113,7 @@ func DeterministicRandom(seedHex string, sequenceNr int, probabilities []float64 hash := h.Sum(nil) x := binary.BigEndian.Uint64(hash[:8]) - // Find the prize index + // Find the selected index for i, t := range thresholds { if x < t { return i, nil From 35838752ad8d25ae020635c90058b13b8fdfe289 Mon Sep 17 00:00:00 2001 From: FTHans Date: Tue, 3 Jun 2025 10:35:10 +0200 Subject: [PATCH 3/9] added default config for seed --- internal/config/config.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/config/config.go b/internal/config/config.go index de9dc0a..597136e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -8,6 +8,7 @@ import ( var ( GRPCPort = flag.Int("grpc-port", 3401, "Port for gRPC server") HTTPPort = flag.Int("http-port", 3402, "Port for HTTP server") + SEEDHEX = flag.String("seed-hex", "0000000000000000000000000000000000000000000000000000000000000000", "Seed for the deterministic random number") ) func init() { From c58881a71dc85d83bd429db25ee9a3c63284d5f0 Mon Sep 17 00:00:00 2001 From: FTHans Date: Tue, 3 Jun 2025 11:20:50 +0200 Subject: [PATCH 4/9] improved on type safety --- README.md | 9 +++++++++ cmd/grpc/main.go | 2 +- cmd/http/main.go | 8 ++++---- cmd/simulator/main.go | 8 ++++---- random.go | 7 +++++-- random_test.go | 4 ++-- 6 files changed, 25 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index ef8690d..6d53fe2 100644 --- a/README.md +++ b/README.md @@ -46,3 +46,12 @@ gosec -exclude-dir=pkg/pb ./... ```bash docker run -p 8081:3402 fasttrack/random http ``` + +## Other + +### Ensuring deterministic results +The results from function DeterministicRandom can be tested for consistency by using the simulator to generate results +and then hashing the result of two runs with the same parameters. +```bash + shasum -a 256 cmd/simulator/results/DeterministicRandom-X.csv +``` diff --git a/cmd/grpc/main.go b/cmd/grpc/main.go index 615bafa..042b8d9 100644 --- a/cmd/grpc/main.go +++ b/cmd/grpc/main.go @@ -93,7 +93,7 @@ func (rs *RandomGRPCServer) GetRandomFloat64(ctx context.Context, req *pb.GetRan } func (rs *RandomGRPCServer) GetDeterministicRandom(ctx context.Context, req *pb.GetDeterministicRandomRequest) (*pb.GetDeterministicRandomResponse, error) { - number, err := random.DeterministicRandom(rs.seed, int(req.Sequence), req.Probabilities) + number, err := random.DeterministicRandom(rs.seed, req.Sequence, req.Probabilities) if err != nil { return nil, err } diff --git a/cmd/http/main.go b/cmd/http/main.go index 5f827aa..31159f9 100644 --- a/cmd/http/main.go +++ b/cmd/http/main.go @@ -98,15 +98,15 @@ func main() { ginEngine.GET("/getDeterministicRandom", func(c *gin.Context) { seed := *config.SEEDHEX - sequence := 0 + sequence := int32(0) sequenceAsStr := c.Query("s") if len(sequenceAsStr) == 0 { c.String(http.StatusBadRequest, "sequence is missing") c.Abort() return } else { - sequenceAsNumber, errAtoi := strconv.Atoi(sequenceAsStr) - if errAtoi != nil { + sequenceAsNumber, errParseInt := strconv.ParseInt(sequenceAsStr, 10, 32) + if errParseInt != nil { c.String(http.StatusBadRequest, "unable to parse sequence as number") c.Abort() return @@ -115,7 +115,7 @@ func main() { c.Abort() return } else { - sequence = sequenceAsNumber + sequence = int32(sequenceAsNumber) } } diff --git a/cmd/simulator/main.go b/cmd/simulator/main.go index 81da5b6..91763ea 100644 --- a/cmd/simulator/main.go +++ b/cmd/simulator/main.go @@ -118,7 +118,7 @@ func main() { continue } - numbersToGenerate, errParseIntCount := strconv.Atoi(res) + numbersToGenerate, errParseIntCount := strconv.ParseInt(res, 10, 32) if errParseIntCount != nil { fmt.Println(res, "is an invalid number, try again") continue @@ -159,7 +159,7 @@ func main() { fmt.Print("generating ", numbersToGenerate, " random numbers... ") - fileName, errGenerate := generateDeterministicRandom(numbersToGenerate, seedHex, probabilities) + fileName, errGenerate := generateDeterministicRandom(int32(numbersToGenerate), seedHex, probabilities) if errGenerate != nil { fmt.Print("error: ", errGenerate) return @@ -256,7 +256,7 @@ func generateUniformInt64(numbersToGenerate int, min int32, max int32) (string, return fileName, nil } -func generateDeterministicRandom(numbersToGenerate int, seed string, probabilities []float64) (string, error) { +func generateDeterministicRandom(numbersToGenerate int32, seed string, probabilities []float64) (string, error) { fileName := fmt.Sprintf("cmd/simulator/results/DeterministicRandom-%v.csv", time.Now().UnixMilli()) f, err := os.Create(filepath.Clean(fileName)) @@ -276,7 +276,7 @@ func generateDeterministicRandom(numbersToGenerate int, seed string, probabiliti return "", errWriteString } - for i := 0; i < numbersToGenerate; i++ { + for i := int32(0); i < numbersToGenerate; i++ { rnd, errRnr := random.DeterministicRandom(seed, i, probabilities) if errRnr != nil { return "", errRnr diff --git a/random.go b/random.go index 0061907..418c533 100644 --- a/random.go +++ b/random.go @@ -59,7 +59,7 @@ func Truncate(val float64, precision int) float64 { // DeterministicRandom creates deterministic random numbers using a seed. // The same seed, sequence number and probabilities generate the same outcome. -func DeterministicRandom(seedHex string, sequence int, probabilities []float64) (int, error) { +func DeterministicRandom(seedHex string, sequence int32, probabilities []float64) (int32, error) { // Validate input if len(seedHex) != 64 { return 0, errors.New("seedHex must be 64 bytes") @@ -116,7 +116,10 @@ func DeterministicRandom(seedHex string, sequence int, probabilities []float64) // Find the selected index for i, t := range thresholds { if x < t { - return i, nil + if i < math.MinInt32 || i > math.MaxInt32 { + return 0, fmt.Errorf("threshold index out of range for Int32") + } + return int32(i), nil } } diff --git a/random_test.go b/random_test.go index ec5f920..452fd0e 100644 --- a/random_test.go +++ b/random_test.go @@ -63,9 +63,9 @@ func Test_Truncate(t *testing.T) { func Test_DeterministicRandom(t *testing.T) { testCases := []struct { seedHex string - sequence int + sequence int32 probabilities []float64 - expectedIndex int + expectedIndex int32 }{ { seedHex: "9912f3bcf715a55ae5c9d47f9f6562599912f3bcf715a55ae5c9d47f9f656259", From 5b0b308a62ee20a77bb86ed713e5ec9b2bbf6bf4 Mon Sep 17 00:00:00 2001 From: FTHans Date: Tue, 3 Jun 2025 11:29:54 +0200 Subject: [PATCH 5/9] updated int32 to int64 for consistency --- cmd/grpc/main.go | 2 +- cmd/http/main.go | 10 +++++----- cmd/simulator/main.go | 8 ++++---- pkg/pb/service.pb.go | 12 ++++++------ pkg/pb/service.proto | 4 ++-- random.go | 6 +++--- random_test.go | 4 ++-- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/cmd/grpc/main.go b/cmd/grpc/main.go index 042b8d9..d2e554e 100644 --- a/cmd/grpc/main.go +++ b/cmd/grpc/main.go @@ -99,6 +99,6 @@ func (rs *RandomGRPCServer) GetDeterministicRandom(ctx context.Context, req *pb. } return &pb.GetDeterministicRandomResponse{ - Number: int32(number), + Number: number, }, nil } diff --git a/cmd/http/main.go b/cmd/http/main.go index 31159f9..5d570d8 100644 --- a/cmd/http/main.go +++ b/cmd/http/main.go @@ -98,24 +98,24 @@ func main() { ginEngine.GET("/getDeterministicRandom", func(c *gin.Context) { seed := *config.SEEDHEX - sequence := int32(0) + sequence := int64(0) sequenceAsStr := c.Query("s") if len(sequenceAsStr) == 0 { c.String(http.StatusBadRequest, "sequence is missing") c.Abort() return } else { - sequenceAsNumber, errParseInt := strconv.ParseInt(sequenceAsStr, 10, 32) + sequenceAsNumber, errParseInt := strconv.ParseInt(sequenceAsStr, 10, 64) if errParseInt != nil { c.String(http.StatusBadRequest, "unable to parse sequence as number") c.Abort() return - } else if sequenceAsNumber < 0 || sequenceAsNumber >= math.MaxInt32 { - c.String(http.StatusBadRequest, "sequence must be between 0 and 2,147,483,647") + } else if sequenceAsNumber < 0 || sequenceAsNumber >= math.MaxInt64 { + c.String(http.StatusBadRequest, "sequence must be between 0 and 9,223,372,036,854,775,806") c.Abort() return } else { - sequence = int32(sequenceAsNumber) + sequence = sequenceAsNumber } } diff --git a/cmd/simulator/main.go b/cmd/simulator/main.go index 91763ea..6121e25 100644 --- a/cmd/simulator/main.go +++ b/cmd/simulator/main.go @@ -118,7 +118,7 @@ func main() { continue } - numbersToGenerate, errParseIntCount := strconv.ParseInt(res, 10, 32) + numbersToGenerate, errParseIntCount := strconv.ParseInt(res, 10, 64) if errParseIntCount != nil { fmt.Println(res, "is an invalid number, try again") continue @@ -159,7 +159,7 @@ func main() { fmt.Print("generating ", numbersToGenerate, " random numbers... ") - fileName, errGenerate := generateDeterministicRandom(int32(numbersToGenerate), seedHex, probabilities) + fileName, errGenerate := generateDeterministicRandom(numbersToGenerate, seedHex, probabilities) if errGenerate != nil { fmt.Print("error: ", errGenerate) return @@ -256,7 +256,7 @@ func generateUniformInt64(numbersToGenerate int, min int32, max int32) (string, return fileName, nil } -func generateDeterministicRandom(numbersToGenerate int32, seed string, probabilities []float64) (string, error) { +func generateDeterministicRandom(numbersToGenerate int64, seed string, probabilities []float64) (string, error) { fileName := fmt.Sprintf("cmd/simulator/results/DeterministicRandom-%v.csv", time.Now().UnixMilli()) f, err := os.Create(filepath.Clean(fileName)) @@ -276,7 +276,7 @@ func generateDeterministicRandom(numbersToGenerate int32, seed string, probabili return "", errWriteString } - for i := int32(0); i < numbersToGenerate; i++ { + for i := int64(0); i < numbersToGenerate; i++ { rnd, errRnr := random.DeterministicRandom(seed, i, probabilities) if errRnr != nil { return "", errRnr diff --git a/pkg/pb/service.pb.go b/pkg/pb/service.pb.go index 101bd8e..948522a 100644 --- a/pkg/pb/service.pb.go +++ b/pkg/pb/service.pb.go @@ -199,7 +199,7 @@ func (x *GetRandomInt64Response) GetNumber() int64 { type GetDeterministicRandomRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - Sequence int32 `protobuf:"varint,1,opt,name=sequence,proto3" json:"sequence,omitempty"` + Sequence int64 `protobuf:"varint,1,opt,name=sequence,proto3" json:"sequence,omitempty"` Probabilities []float64 `protobuf:"fixed64,2,rep,packed,name=probabilities,proto3" json:"probabilities,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -235,7 +235,7 @@ func (*GetDeterministicRandomRequest) Descriptor() ([]byte, []int) { return file_pkg_pb_service_proto_rawDescGZIP(), []int{4} } -func (x *GetDeterministicRandomRequest) GetSequence() int32 { +func (x *GetDeterministicRandomRequest) GetSequence() int64 { if x != nil { return x.Sequence } @@ -251,7 +251,7 @@ func (x *GetDeterministicRandomRequest) GetProbabilities() []float64 { type GetDeterministicRandomResponse struct { state protoimpl.MessageState `protogen:"open.v1"` - Number int32 `protobuf:"varint,1,opt,name=number,proto3" json:"number,omitempty"` + Number int64 `protobuf:"varint,1,opt,name=number,proto3" json:"number,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -286,7 +286,7 @@ func (*GetDeterministicRandomResponse) Descriptor() ([]byte, []int) { return file_pkg_pb_service_proto_rawDescGZIP(), []int{5} } -func (x *GetDeterministicRandomResponse) GetNumber() int32 { +func (x *GetDeterministicRandomResponse) GetNumber() int64 { if x != nil { return x.Number } @@ -307,10 +307,10 @@ const file_pkg_pb_service_proto_rawDesc = "" + "\x16GetRandomInt64Response\x12\x16\n" + "\x06number\x18\x01 \x01(\x03R\x06number\"a\n" + "\x1dGetDeterministicRandomRequest\x12\x1a\n" + - "\bsequence\x18\x01 \x01(\x05R\bsequence\x12$\n" + + "\bsequence\x18\x01 \x01(\x03R\bsequence\x12$\n" + "\rprobabilities\x18\x02 \x03(\x01R\rprobabilities\"8\n" + "\x1eGetDeterministicRandomResponse\x12\x16\n" + - "\x06number\x18\x01 \x01(\x05R\x06number2\x99\x02\n" + + "\x06number\x18\x01 \x01(\x03R\x06number2\x99\x02\n" + "\x06Random\x12U\n" + "\x10GetRandomFloat64\x12\x1f.random.GetRandomFloat64Request\x1a .random.GetRandomFloat64Response\x12O\n" + "\x0eGetRandomInt64\x12\x1d.random.GetRandomInt64Request\x1a\x1e.random.GetRandomInt64Response\x12g\n" + diff --git a/pkg/pb/service.proto b/pkg/pb/service.proto index cd64c07..5fbabd7 100644 --- a/pkg/pb/service.proto +++ b/pkg/pb/service.proto @@ -27,10 +27,10 @@ message GetRandomInt64Response { } message GetDeterministicRandomRequest { - int32 sequence = 1; + int64 sequence = 1; repeated double probabilities = 2; } message GetDeterministicRandomResponse { - int32 number = 1; + int64 number = 1; } diff --git a/random.go b/random.go index 418c533..2d366fa 100644 --- a/random.go +++ b/random.go @@ -59,7 +59,7 @@ func Truncate(val float64, precision int) float64 { // DeterministicRandom creates deterministic random numbers using a seed. // The same seed, sequence number and probabilities generate the same outcome. -func DeterministicRandom(seedHex string, sequence int32, probabilities []float64) (int32, error) { +func DeterministicRandom(seedHex string, sequence int64, probabilities []float64) (int64, error) { // Validate input if len(seedHex) != 64 { return 0, errors.New("seedHex must be 64 bytes") @@ -116,10 +116,10 @@ func DeterministicRandom(seedHex string, sequence int32, probabilities []float64 // Find the selected index for i, t := range thresholds { if x < t { - if i < math.MinInt32 || i > math.MaxInt32 { + if i < math.MinInt64 || i > math.MaxInt64 { return 0, fmt.Errorf("threshold index out of range for Int32") } - return int32(i), nil + return int64(i), nil } } diff --git a/random_test.go b/random_test.go index 452fd0e..727959d 100644 --- a/random_test.go +++ b/random_test.go @@ -63,9 +63,9 @@ func Test_Truncate(t *testing.T) { func Test_DeterministicRandom(t *testing.T) { testCases := []struct { seedHex string - sequence int32 + sequence int64 probabilities []float64 - expectedIndex int32 + expectedIndex int64 }{ { seedHex: "9912f3bcf715a55ae5c9d47f9f6562599912f3bcf715a55ae5c9d47f9f656259", From dfc74e5c2a06f13e16110ac912f07a1b344c37f5 Mon Sep 17 00:00:00 2001 From: FTHans Date: Tue, 3 Jun 2025 12:07:35 +0200 Subject: [PATCH 6/9] crash on startup when using bad or default seed --- cmd/grpc/main.go | 9 ++++++++- cmd/http/main.go | 10 +++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/cmd/grpc/main.go b/cmd/grpc/main.go index d2e554e..4e9775e 100644 --- a/cmd/grpc/main.go +++ b/cmd/grpc/main.go @@ -17,6 +17,13 @@ import ( ) func main() { + seed := *config.SEEDHEX + if len(seed) != 64 { + panic("seed must be 64 hex characters") + } else if seed == "0000000000000000000000000000000000000000000000000000000000000000" { + panic("unique seed must set in the config") + } + recoveryOpts := []grpc_recovery.Option{ grpc_recovery.WithRecoveryHandler(func(p interface{}) (err error) { slog.Error("[PANIC] recovered panic", "error", p, "stacktrace", string(debug.Stack())) @@ -37,7 +44,7 @@ func main() { reflection.Register(s) - randomServer := NewRandomGRPCServer(*config.SEEDHEX) + randomServer := NewRandomGRPCServer(seed) pb.RegisterRandomServer(s, randomServer) lis, errListen := net.Listen("tcp", fmt.Sprintf(":%v", *config.GRPCPort)) diff --git a/cmd/http/main.go b/cmd/http/main.go index 5d570d8..b063bfb 100644 --- a/cmd/http/main.go +++ b/cmd/http/main.go @@ -14,8 +14,14 @@ import ( ) func main() { - gin.SetMode(gin.ReleaseMode) + seed := *config.SEEDHEX + if len(seed) != 64 { + panic("seed must be 64 hex characters") + } else if seed == "0000000000000000000000000000000000000000000000000000000000000000" { + panic("unique seed must set in the config") + } + gin.SetMode(gin.ReleaseMode) ginEngine := gin.New() ginEngine.Use(gin.Recovery()) @@ -96,8 +102,6 @@ func main() { }) ginEngine.GET("/getDeterministicRandom", func(c *gin.Context) { - seed := *config.SEEDHEX - sequence := int64(0) sequenceAsStr := c.Query("s") if len(sequenceAsStr) == 0 { From d26336e36ee8f844be13b53f10a9b8d0c3c65a97 Mon Sep 17 00:00:00 2001 From: FTHans Date: Tue, 3 Jun 2025 12:33:30 +0200 Subject: [PATCH 7/9] updated readme with docker example with seed --- README.md | 4 ++-- cmd/grpc/main.go | 2 +- cmd/http/main.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6d53fe2..9ee35e9 100644 --- a/README.md +++ b/README.md @@ -41,10 +41,10 @@ gosec -exclude-dir=pkg/pb ./... ### Run ```bash - docker run -p 8080:3401 fasttrack/random grpc + docker run -p 8080:3401 -e SEED_HEX=0000000000000000000000000000000000000000000000000000000000000000 fasttrack/random grpc ``` ```bash - docker run -p 8081:3402 fasttrack/random http + docker run -p 8081:3402 -e SEED_HEX=0000000000000000000000000000000000000000000000000000000000000000 fasttrack/random http ``` ## Other diff --git a/cmd/grpc/main.go b/cmd/grpc/main.go index 4e9775e..a556d09 100644 --- a/cmd/grpc/main.go +++ b/cmd/grpc/main.go @@ -21,7 +21,7 @@ func main() { if len(seed) != 64 { panic("seed must be 64 hex characters") } else if seed == "0000000000000000000000000000000000000000000000000000000000000000" { - panic("unique seed must set in the config") + panic("a unique seed value is required") } recoveryOpts := []grpc_recovery.Option{ diff --git a/cmd/http/main.go b/cmd/http/main.go index b063bfb..9d89610 100644 --- a/cmd/http/main.go +++ b/cmd/http/main.go @@ -18,7 +18,7 @@ func main() { if len(seed) != 64 { panic("seed must be 64 hex characters") } else if seed == "0000000000000000000000000000000000000000000000000000000000000000" { - panic("unique seed must set in the config") + panic("a unique seed value is required") } gin.SetMode(gin.ReleaseMode) From 766e28069ca8e1cb9de32fe8231990eb5ef0e3ed Mon Sep 17 00:00:00 2001 From: FTHans Date: Tue, 3 Jun 2025 13:11:18 +0200 Subject: [PATCH 8/9] added http example to readme --- README.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9ee35e9..2d8c7d6 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,26 @@ gosec -exclude-dir=pkg/pb ./... ## Other -### Ensuring deterministic results +### Example HTTP Requests +```http + GET http://localhost:8081/getRandomFloat64 +``` +```http + GET http://localhost:8081/getRandomInt64?min=0&max=10 + + Querystring parameters: + min - minimum number (inclusive) + max - maximum number (inclusive) +``` +```http + GET http://localhost:8081/getDeterministicRandom?s=42&p=0.01,0.4,0.59 + + Querystring parameters: + (s)equence - the sequence number of the random number + (p)robabilities - the set of probabilities to select an index from +``` + +### Validating deterministic results The results from function DeterministicRandom can be tested for consistency by using the simulator to generate results and then hashing the result of two runs with the same parameters. ```bash From e3b7f89d0d6b792de6949550a53f93fe8cab90eb Mon Sep 17 00:00:00 2001 From: FTHans Date: Tue, 3 Jun 2025 13:16:28 +0200 Subject: [PATCH 9/9] updated readme with hex generation example --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 2d8c7d6..f1fb4fc 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,13 @@ gosec -exclude-dir=pkg/pb ./... (p)robabilities - the set of probabilities to select an index from ``` +### Generating a seed +There are several sites where a hex code can be generated. + +Example: https://codebeautify.org/generate-random-hexadecimal-numbers + +Simply set 'length of hex number' to 64 and generate one. + ### Validating deterministic results The results from function DeterministicRandom can be tested for consistency by using the simulator to generate results and then hashing the result of two runs with the same parameters.