267 lines
8.4 KiB
Go
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
|