diff --git a/tests/event_smoke/api_client.go b/tests/event_smoke/api_client.go index 2bd1689..ea0c008 100644 --- a/tests/event_smoke/api_client.go +++ b/tests/event_smoke/api_client.go @@ -109,12 +109,18 @@ func getDecryptionKey(cfg *Config, identity string, eon int64) (key, msg string, var m map[string]any _ = json.Unmarshal(body, &m) - if v := str(m["decryption_key"]); strings.HasPrefix(v, "0x") && len(v) > 2 { - return v, "", true + if v := str(m["decryption_key"]); v != "" { + if strings.HasPrefix(v, "0x") && len(v) > 2 { + return v, "", true + } + return "", fmt.Sprintf("decryption_key present but unexpected format: %q", v), false } if msgObj, ok := m["message"].(map[string]any); ok { - if v := str(msgObj["decryption_key"]); strings.HasPrefix(v, "0x") && len(v) > 2 { - return v, "", true + if v := str(msgObj["decryption_key"]); v != "" { + if strings.HasPrefix(v, "0x") && len(v) > 2 { + return v, "", true + } + return "", fmt.Sprintf("decryption_key present but unexpected format: %q", v), false } } diff --git a/tests/event_smoke/case_loader.go b/tests/event_smoke/case_loader.go index 859c6d8..9b29cd9 100644 --- a/tests/event_smoke/case_loader.go +++ b/tests/event_smoke/case_loader.go @@ -16,6 +16,7 @@ type jsonCase struct { EmitSig string `json:"emitSig"` EmitArgs []string `json:"emitArgs"` Expected string `json:"expected"` // "pass" | "fail" + MultiReg int `json:"multiReg,omitempty"` } var varRe = regexp.MustCompile(`\$\{([A-Z0-9_]+)\}`) @@ -41,6 +42,7 @@ func LoadCasesFromJSON(path string, vars map[string]string) ([]TestCase, error) EmitArg: make([]string, 0, len(c.EmitArgs)), Args: make([]EventArg, 0, len(c.Args)), ExpectKey: !strings.EqualFold(strings.TrimSpace(c.Expected), "fail"), + MultiReg: c.MultiReg, } for _, a := range c.EmitArgs { tc.EmitArg = append(tc.EmitArg, expand(a, vars)) diff --git a/tests/event_smoke/runner.go b/tests/event_smoke/runner.go index fff0314..2cd4229 100644 --- a/tests/event_smoke/runner.go +++ b/tests/event_smoke/runner.go @@ -26,6 +26,10 @@ func runCase(cfg *Config, tc TestCase) Result { meta.TriggerDef = td logf(cfg, "[%s] trigger=%s", tc.Name, shortHex(td, 26)) + if tc.MultiReg > 1 { + return runMultiReg(cfg, tc, td) + } + fmt.Printf("[%s] register\n", tc.Name) identity, eon, regTx, prefix, err := registerIdentity(cfg, td) if err != nil { @@ -133,6 +137,146 @@ func runCase(cfg *Config, tc TestCase) Result { } } +func runMultiReg(cfg *Config, tc TestCase, triggerDef string) Result { + n := tc.MultiReg + type regEntry struct { + identity string + eon int64 + txHash string + } + regs := make([]regEntry, 0, n) + + for i := 0; i < n; i++ { + fmt.Printf("[%s] register %d/%d\n", tc.Name, i+1, n) + identity, eon, regTx, prefix, err := registerIdentity(cfg, triggerDef) + if err != nil { + return Result{tc.Name, "FAIL", fmt.Sprintf("register %d: %s", i+1, err.Error())} + } + logf(cfg, "[%s] reg[%d] identity=%s eon=%d prefix=%s", tc.Name, i+1, identity, eon, prefix) + regs = append(regs, regEntry{identity, eon, regTx}) + } + + if cfg.WaitRegReceipt { + fmt.Printf("[%s] waiting for %d registration receipts (parallel)\n", tc.Name, n) + type receiptResult struct { + block int64 + err error + } + ch := make(chan receiptResult, n) + for _, reg := range regs { + reg := reg + go func() { + block, err := waitReceiptBlock(cfg, reg.txHash) + ch <- receiptResult{block, err} + }() + } + maxBlock := int64(0) + for range regs { + r := <-ch + if r.err != nil { + return Result{tc.Name, "FAIL", "registration receipt: " + r.err.Error()} + } + if r.block > maxBlock { + maxBlock = r.block + } + } + _ = waitBlockGreater(cfg, maxBlock) + } else { + fmt.Printf("[%s] all %d registration txs sent (sleep %s)\n", tc.Name, n, cfg.RegistrationDelay) + time.Sleep(cfg.RegistrationDelay) + } + + fmt.Printf("[%s] emit\n", tc.Name) + evTx, err := emitEvent(cfg, tc.EmitSig, tc.EmitArg) + if err != nil { + return Result{tc.Name, "FAIL", "emit: " + err.Error()} + } + logf(cfg, "[%s] emitTx=%s sig=%s args=%v", tc.Name, evTx, tc.EmitSig, tc.EmitArg) + + evBlock, err := waitReceiptBlock(cfg, evTx) + if err != nil { + return Result{tc.Name, "FAIL", "event receipt: " + err.Error()} + } + logf(cfg, "[%s] eventBlock=%d", tc.Name, evBlock) + + fmt.Printf("[%s] poll %d keys\n", tc.Name, n) + deadline := time.Now().Add(time.Duration(cfg.PollSeconds) * time.Second) + keys := make(map[string]string, n) // identity -> key + timeouts := make(map[string]int, n) + + for time.Now().Before(deadline) && len(keys) < n { + for _, reg := range regs { + if _, found := keys[reg.identity]; found { + continue + } + key, msg, ok := getDecryptionKey(cfg, reg.identity, reg.eon) + if ok { + keys[reg.identity] = key + logf(cfg, "[%s] got key for identity=%s key=%s", tc.Name, shortHex(reg.identity, 10), shortHex(key, 18)) + continue + } + logf(cfg, "[%s] pending identity=%s msg=%s", tc.Name, shortHex(reg.identity, 10), msg) + + if strings.Contains(strings.ToLower(msg), "timeout") { + timeouts[reg.identity]++ + if timeouts[reg.identity] >= cfg.MaxConsecTimeouts { + return Result{ + Name: tc.Name, + Status: "FAIL", + Reason: fmt.Sprintf("aborted after %d timeouts for identity %s: %s", timeouts[reg.identity], reg.identity, msg), + } + } + } else { + timeouts[reg.identity] = 0 + } + + if !isTransient(msg) && !isTerminalNotFound(msg) { + return Result{ + Name: tc.Name, + Status: "FAIL", + Reason: fmt.Sprintf("non-transient error for identity %s: %s", reg.identity, msg), + } + } + } + if len(keys) < n { + time.Sleep(time.Duration(cfg.PollInterval) * time.Second) + } + } + + if len(keys) < n { + missing := make([]string, 0, n-len(keys)) + for _, reg := range regs { + if _, found := keys[reg.identity]; !found { + missing = append(missing, shortHex(reg.identity, 10)) + } + } + return Result{ + Name: tc.Name, + Status: "FAIL", + Reason: fmt.Sprintf("timeout: only %d/%d keys received, missing identities: %v", len(keys), n, missing), + } + } + + // assert all keys are distinct + seen := make(map[string]string, n) // key -> identity + for identity, key := range keys { + if prev, dup := seen[key]; dup { + return Result{ + Name: tc.Name, + Status: "FAIL", + Reason: fmt.Sprintf("duplicate key %s for identities %s and %s", shortHex(key, 18), shortHex(prev, 10), shortHex(identity, 10)), + } + } + seen[key] = identity + } + + return Result{ + Name: tc.Name, + Status: "PASS", + Reason: fmt.Sprintf("received %d distinct decryption keys for %d registrations", n, n), + } +} + func isTerminalNotFound(msg string) bool { m := strings.ToLower(msg) return strings.Contains(m, "http 404") || diff --git a/tests/event_smoke/testdata/cases.chiado.json b/tests/event_smoke/testdata/cases.chiado.json index b432e6e..9011236 100644 --- a/tests/event_smoke/testdata/cases.chiado.json +++ b/tests/event_smoke/testdata/cases.chiado.json @@ -11,6 +11,19 @@ "emitArgs": ["${FROM_ADDR}", "${DEST_ADDR}", "${TRANSFER_VALUE}"], "expected": "pass" }, + { + "name": "transfer_like_multi", + "description": "TransferLike multi-registration: 10 identities for the same event trigger, all should receive distinct decryption keys", + "eventSig": "event TransferLike(address indexed from, address indexed to, uint256 value)", + "args": [ + { "name": "to", "op": "eq", "bytes": "${DEST_ADDR}" }, + { "name": "value", "op": "gte", "number": "${TRANSFER_VALUE}" } + ], + "emitSig": "emitTransferLike(address,address,uint256)", + "emitArgs": ["${FROM_ADDR}", "${DEST_ADDR}", "${TRANSFER_VALUE}"], + "multiReg": 100, + "expected": "pass" + }, { "name": "static_args", "description": "StaticArgs baseline amount >= 42", diff --git a/tests/event_smoke/types.go b/tests/event_smoke/types.go index 53e06b8..df89779 100644 --- a/tests/event_smoke/types.go +++ b/tests/event_smoke/types.go @@ -33,6 +33,7 @@ type TestCase struct { EmitSig string EmitArg []string ExpectKey bool + MultiReg int // >1: register this many identities, emit once, assert all get distinct keys } type Result struct {