267 lines
8.4 KiB
Go

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 <hex> --issue <id> --user <id> --salt <hex> --weights <list>")
fmt.Println(" verify-ichiban --seed <hex> --issue <id> --slot <num> --rewards <list>")
fmt.Println(" inspect-ichiban --seed <hex> --issue <id> --rewards <list> (Dump all slots)")
fmt.Println(" verify-file --path <json_file> [--weights <list>] (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