package main import ( "crypto/hmac" "crypto/sha256" "encoding/binary" "encoding/json" "flag" "fmt" "os" "strings" ) func main() { // Subcommands verifyUnlimitedCmd := flag.NewFlagSet("verify-unlimited", flag.ExitOnError) verifyIchibanCmd := flag.NewFlagSet("verify-ichiban", flag.ExitOnError) inspectIchibanCmd := flag.NewFlagSet("inspect-ichiban", flag.ExitOnError) verifyFileCmd := flag.NewFlagSet("verify-file", flag.ExitOnError) // Unlimited Args vuSeed := verifyUnlimitedCmd.String("seed", "", "Server Seed (Hex)") vuIssue := verifyUnlimitedCmd.Int64("issue", 0, "Issue ID") vuUser := verifyUnlimitedCmd.Int64("user", 0, "User ID") vuSalt := verifyUnlimitedCmd.String("salt", "", "Salt (Hex)") vuWeights := verifyUnlimitedCmd.String("weights", "", "Rewards (Format: ID:Weight,ID:Weight...)") // Ichiban Args viSeed := verifyIchibanCmd.String("seed", "", "Server Seed (Hex)") viIssue := verifyIchibanCmd.Int64("issue", 0, "Issue ID") viSlot := verifyIchibanCmd.Int("slot", 0, "Selected Slot (1-based)") viRewards := verifyIchibanCmd.String("rewards", "", "Rewards (Format: ID:Count,ID:Count...)") // Inspect Ichiban Args iiSeed := inspectIchibanCmd.String("seed", "", "Server Seed (Hex)") iiIssue := inspectIchibanCmd.Int64("issue", 0, "Issue ID") iiRewards := inspectIchibanCmd.String("rewards", "", "Rewards (Format: ID:Count,ID:Count...)") // JSON File Args vfPath := verifyFileCmd.String("path", "", "Path to JSON receipt file") vfWeights := verifyFileCmd.String("weights", "", "Global Weights for Unlimited (Format: ID:Weight...)") vfRewards := verifyFileCmd.String("rewards", "", "Global Rewards for Ichiban (Format: ID:Count...)") if len(os.Args) < 2 { printUsage() return } switch os.Args[1] { case "verify-unlimited": verifyUnlimitedCmd.Parse(os.Args[2:]) runUnlimited(*vuSeed, *vuIssue, *vuUser, *vuSalt, *vuWeights) case "verify-ichiban": verifyIchibanCmd.Parse(os.Args[2:]) runIchiban(*viSeed, *viIssue, *viSlot, *viRewards) case "inspect-ichiban": inspectIchibanCmd.Parse(os.Args[2:]) runInspectIchiban(*iiSeed, *iiIssue, *iiRewards) case "verify-file": verifyFileCmd.Parse(os.Args[2:]) runVerifyFile(*vfPath, *vfWeights, *vfRewards) default: printUsage() } } func printUsage() { fmt.Println("BindBox Lottery Verifier Tool (v1.2)") fmt.Println("\nUsage:") fmt.Println(" verify-unlimited --seed --issue --user --salt --weights ") fmt.Println(" verify-ichiban --seed --issue --slot --rewards ") fmt.Println(" inspect-ichiban --seed --issue --rewards (Dump all slots)") fmt.Println(" verify-file --path [--weights ] (Load from JSON, support array)") fmt.Println("\nExample Unlimited:") fmt.Println(" verify-unlimited --seed aabbcc... --issue 1001 --user 888 --salt 1234... --weights 1:10,2:50,3:100") fmt.Println("\nExample File (with global weights):") fmt.Println(" verify-file --path receipts.json --weights \"280:88200,281:100...\"") } func runUnlimited(seed string, issue int64, user int64, salt string, weightsStr string) { if seed == "" || issue == 0 || user == 0 || salt == "" || weightsStr == "" { fmt.Println("Error: Missing required arguments.") return } rewards, err := ParseRewardsString(weightsStr) if err != nil { fmt.Printf("Error parsing weights: %v\n", err) return } fmt.Println("========================================") fmt.Println(" UNLIMITED LOTTERY VERIFICATION ") fmt.Println("========================================") fmt.Printf("Server Seed : %s\n", seed) fmt.Printf("Issue ID : %d\n", issue) fmt.Printf("User ID : %d\n", user) fmt.Printf("Salt : %s\n", salt) fmt.Println("----------------------------------------") id, log, err := VerifyUnlimited(seed, issue, user, salt, rewards) if err != nil { fmt.Printf("Verification FAILED: %v\n", err) return } fmt.Println(log) fmt.Println("----------------------------------------") fmt.Printf("VERIFIED RESULT: Reward ID = %d\n", id) fmt.Println("========================================") } func runIchiban(seed string, issue int64, slot int, rewardsStr string) { if seed == "" || issue == 0 || slot == 0 || rewardsStr == "" { fmt.Println("Error: Missing required arguments.") return } rewards, err := ParseRewardsString(rewardsStr) if err != nil { fmt.Printf("Error parsing rewards: %v\n", err) return } fmt.Println("========================================") fmt.Println(" ICHIBAN LOTTERY VERIFICATION ") fmt.Println("========================================") fmt.Printf("Server Seed : %s\n", seed) fmt.Printf("Issue ID : %d\n", issue) fmt.Printf("Values : %d unique items expanded\n", len(rewards)) fmt.Println("----------------------------------------") id, log, err := VerifyIchiban(seed, issue, slot, rewards) if err != nil { fmt.Printf("Verification FAILED: %v\n", err) return } fmt.Println(log) fmt.Println("----------------------------------------") fmt.Printf("VERIFIED RESULT: Reward ID = %d\n", id) fmt.Println("========================================") } func runInspectIchiban(seed string, issue int64, rewardsStr string) { if seed == "" || issue == 0 || rewardsStr == "" { fmt.Println("Error: Missing required arguments.") return } rewards, err := ParseRewardsString(rewardsStr) if err != nil { fmt.Printf("Error parsing rewards: %v\n", err) return } fmt.Println("========================================") fmt.Println(" ICHIBAN GLOBAL INSPECTION ") fmt.Println("========================================") fmt.Printf("Server Seed : %s\n", seed) fmt.Printf("Issue ID : %d\n", issue) fmt.Println("----------------------------------------") var slots []RewardItem for _, r := range rewards { for k := 0; k < r.Count; k++ { slots = append(slots, r) } } totalSlots := len(slots) // Shuffle seedKey, _ := decodeHex(seed) // Create indices mapping indices := make([]int, totalSlots) for i := 0; i < totalSlots; i++ { indices[i] = i } mac := hmac.New(sha256.New, seedKey) // Reconstruct actual items workingSlots := make([]RewardItem, totalSlots) copy(workingSlots, slots) for i := totalSlots - 1; i > 0; i-- { mac.Reset() mac.Write([]byte(fmt.Sprintf("shuffle:%d|issue:%d", i, issue))) sum := mac.Sum(nil) j := int(binary.BigEndian.Uint64(sum[:8]) % uint64(i+1)) workingSlots[i], workingSlots[j] = workingSlots[j], workingSlots[i] } // Print Grid fmt.Printf("%-10s | %-10s | %s\n", "SLOT (NO.)", "REWARD ID", "NAME") fmt.Println(strings.Repeat("-", 40)) for i, item := range workingSlots { fmt.Printf("%-10d | %-10d | %s\n", i+1, item.ID, item.Name) } fmt.Println("========================================") } func runVerifyFile(path string, globalWeights string, globalRewards string) { if path == "" { fmt.Println("Error: Missing file path.") return } data, err := os.ReadFile(path) if err != nil { fmt.Printf("Error reading file: %v\n", err) return } type Receipt struct { Mode string `json:"mode"` Seed string `json:"seed"` IssueID int64 `json:"issue_id"` UserID int64 `json:"user_id"` Salt string `json:"salt"` Weights string `json:"weights"` SlotIndex int `json:"slot_index"` Rewards string `json:"rewards"` } var receipts []Receipt // Try parsing as array first if err := json.Unmarshal(data, &receipts); err != nil { // Try parsing as single object var single Receipt if err2 := json.Unmarshal(data, &single); err2 != nil { fmt.Printf("Error parsing JSON (tried both array and object): %v\n", err) return } receipts = append(receipts, single) } fmt.Printf("Loaded %d receipt(s) from file.\n", len(receipts)) for i, r := range receipts { fmt.Printf("\n>>> Verifying Receipt #%d <<<\n", i+1) if r.Mode == "unlimited" { w := r.Weights if w == "" { w = globalWeights if w != "" { fmt.Println("(Using global weights)") } } runUnlimited(r.Seed, r.IssueID, r.UserID, r.Salt, w) } else if r.Mode == "ichiban" { rew := r.Rewards if rew == "" { rew = globalRewards if rew != "" { fmt.Println("(Using global rewards)") } } runIchiban(r.Seed, r.IssueID, r.SlotIndex, rew) } else { fmt.Printf("Unknown or missing mode in JSON: %s\n", r.Mode) } } } // Helper to decode Hex inside main pkg if needed, // but verify.go already has decodeHex. // As they are in the same package 'main', main.go can call functions in verify.go