Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/ethereum/go-ethereum v1.15.11
github.com/gin-contrib/cors v1.7.3
github.com/jackc/pgx/v5 v5.7.1
github.com/joho/godotenv v1.5.1
github.com/libp2p/go-libp2p-pubsub v0.13.1
github.com/prometheus/client_golang v1.22.0
github.com/shutter-network/contracts/v2 v2.0.0-beta.2.0.20250908105003-7e53b1579b04
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,8 @@ github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPw
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U=
github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
Expand Down
16 changes: 16 additions & 0 deletions tests/event_smoke/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
API_BASE_URL=https://shutter-api.chiado.staging.shutter.network/api
RPC_URL=https://rpc.chiadochain.net
PRIVATE_KEY=0xYOUR_PRIVATE_KEY
PLAYGROUND_ADDR=0x0B05BC0BCe48efb0Dd0777C057D87f9Bf66839b4
DEST_ADDR=0x1111111111111111111111111111111111111111
FROM_ADDR=0x1111111111111111111111111111111111111111
TRANSFER_VALUE=2
TTL=120
POLL_SECONDS=130
POLL_INTERVAL=2
VERBOSE=true
WAIT_REGISTRATION_RECEIPT=false
REGISTRATION_DELAY_SECONDS=2
MAX_CONSEC_TIMEOUTS=5
AUTH_HEADER=
FOUNDRY_DISABLE_NIGHTLY_WARNING=1
76 changes: 76 additions & 0 deletions tests/event_smoke/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Event Smoke

Live smoke tests for event-based identity registration and decryption key generation.

## Classification

- This suite is a **smoke test** by intent.
- It is also a **live integration test** by execution model.
- It is intended to run against a **reachable RPC endpoint** and a **running keyper set** (with DKG completed for the target eon).
- It is excluded from default test runs via `//go:build live`.

## Tooling

- To run the live tests: `cast`, `openssl`
- To deploy the playground contract with the helper script: `forge`

## .env support

The live test auto-loads `.env` from:

- `tests/event_smoke/.env`

## Deploy playground contract

For Chiado, a playground contract is already deployed at:

`0x0B05BC0BCe48efb0Dd0777C057D87f9Bf66839b4`

You can reuse it directly:

```bash
export PLAYGROUND_ADDR=0x0B05BC0BCe48efb0Dd0777C057D87f9Bf66839b4
```

If you want a fresh deployment, run:

```bash
PRIVATE_KEY=0x... RPC_URL=https://rpc.chiadochain.net \
./tests/event_smoke/scripts/deploy_event_playground.sh
```

Use the returned address as PLAYGROUND_ADDR.

## Run

```bash
go test -tags=live ./tests/event_smoke -v
```

Run selected cases only:

```bash
CASES=transfer_like,indexed_dynamic_note_eq go test -tags=live ./tests/event_smoke -v
```

## Required env vars

- `API_BASE_URL`
- `RPC_URL`
- `PRIVATE_KEY`
- `PLAYGROUND_ADDR`
- `DEST_ADDR`

## Optional env vars

- `FROM_ADDR`
- `TRANSFER_VALUE`
- `TTL`
- `POLL_SECONDS`
- `POLL_INTERVAL`
- `VERBOSE`
- `WAIT_REGISTRATION_RECEIPT`
- `REGISTRATION_DELAY_SECONDS`
- `MAX_CONSEC_TIMEOUTS`
- `AUTH_HEADER`
- `CASES_FILE` (default: `testdata/cases.chiado.json`)
182 changes: 182 additions & 0 deletions tests/event_smoke/api_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package eventsmoke

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)

func compileTrigger(cfg *Config, event string, args []EventArg) (string, error) {
req := compileReq{
Contract: cfg.PlaygroundAddr,
EventSig: event,
Args: args,
}

var payload map[string]any
if err := postJSON(cfg, "/event/compile_trigger_definition", req, &payload); err != nil {
return "", err
}

if v := str(payload["trigger_definition"]); v != "" {
return v, nil
}
if v := str(payload["triggerDefinition"]); v != "" {
return v, nil
}
if msgObj, ok := payload["message"].(map[string]any); ok {
if v := str(msgObj["trigger_definition"]); v != "" {
return v, nil
}
if v := str(msgObj["triggerDefinition"]); v != "" {
return v, nil
}
}

return "", errors.New(extractErr(payload))
}

func registerIdentity(cfg *Config, triggerDef string) (identity string, eon int64, txHash string, prefix string, err error) {
randHex, err := runCmd("openssl", "rand", "-hex", "32")
if err != nil {
return "", 0, "", "", err
}
prefix = "0x" + strings.TrimSpace(randHex)

req := registerReq{
TriggerDefinition: triggerDef,
IdentityPrefix: prefix,
TTL: cfg.TTL,
}

var payload map[string]any
if err := postJSON(cfg, "/event/register_identity", req, &payload); err != nil {
return "", 0, "", "", err
}

root := payload
if m, ok := payload["message"].(map[string]any); ok {
root = m
}

identity = str(root["identity"])
txHash = str(root["tx_hash"])
eon = toInt64(root["eon"])

if identity == "" || txHash == "" || eon == 0 {
return "", 0, "", "", errors.New(extractErr(payload))
}
return
}

func getDecryptionKey(cfg *Config, identity string, eon int64) (key, msg string, ok bool) {
u, _ := url.Parse(cfg.APIBase + "/event/get_decryption_key")
q := u.Query()
q.Set("identity", identity)
q.Set("eon", fmt.Sprint(eon))
u.RawQuery = q.Encode()

ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel()

req, _ := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
applyAuthHeader(req, cfg.AuthHeader)

resp, err := cfg.HTTPClient.Do(req)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) || strings.Contains(strings.ToLower(err.Error()), "timeout") {
return "", "api timeout (keyper fallback likely hanging)", false
}
return "", err.Error(), false
}
defer resp.Body.Close()

body, _ := io.ReadAll(resp.Body)
raw := strings.TrimSpace(string(body))
logf(cfg, "GET %s status=%d body=%s", u.String(), resp.StatusCode, raw)

if strings.HasPrefix(raw, "0x") && len(raw) > 2 {
return raw, "", true
}

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 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 resp.StatusCode >= 400 {
return "", fmt.Sprintf("http %d: %s", resp.StatusCode, raw), false
}

e := extractErr(m)
if e == "unknown error" && raw != "" {
e = raw
}
return "", e, false
}

func postJSON(cfg *Config, path string, body any, out any) error {
reqBytes, _ := json.Marshal(body)
fullURL := cfg.APIBase + path

req, _ := http.NewRequest(http.MethodPost, fullURL, bytes.NewReader(reqBytes))
req.Header.Set("Content-Type", "application/json")
applyAuthHeader(req, cfg.AuthHeader)

logf(cfg, "POST %s body=%s", fullURL, string(reqBytes))
resp, err := cfg.HTTPClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()

respBytes, _ := io.ReadAll(resp.Body)
logf(cfg, "POST %s status=%d body=%s", fullURL, resp.StatusCode, strings.TrimSpace(string(respBytes)))

_ = json.Unmarshal(respBytes, out)
if resp.StatusCode >= 400 {
return fmt.Errorf("http %d: %s", resp.StatusCode, strings.TrimSpace(string(respBytes)))
}
return nil
}

func applyAuthHeader(req *http.Request, authHeader string) {
if strings.TrimSpace(authHeader) == "" {
return
}
p := strings.SplitN(authHeader, ":", 2)
if len(p) != 2 {
return
}
req.Header.Set(strings.TrimSpace(p[0]), strings.TrimSpace(p[1]))
}

func extractErr(m map[string]any) string {
if s := str(m["description"]); s != "" {
return s
}
if s := str(m["error"]); s != "" {
return s
}
if s := str(m["message"]); s != "" {
return s
}
if errs, ok := m["errors"].([]any); ok && len(errs) > 0 {
return fmt.Sprint(errs[0])
}
return "unknown error"
}
75 changes: 75 additions & 0 deletions tests/event_smoke/case_loader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package eventsmoke

import (
"encoding/json"
"fmt"
"os"
"regexp"
"strings"
)

type jsonCase struct {
Name string `json:"name"`
Description string `json:"description"`
EventSig string `json:"eventSig"`
Args []EventArg `json:"args"`
EmitSig string `json:"emitSig"`
EmitArgs []string `json:"emitArgs"`
Expected string `json:"expected"` // "pass" | "fail"
}

var varRe = regexp.MustCompile(`\$\{([A-Z0-9_]+)\}`)

func LoadCasesFromJSON(path string, vars map[string]string) ([]TestCase, error) {
b, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read cases file: %w", err)
}

var raw []jsonCase
if err := json.Unmarshal(b, &raw); err != nil {
return nil, fmt.Errorf("parse cases json: %w", err)
}

out := make([]TestCase, 0, len(raw))
for _, c := range raw {
tc := TestCase{
Name: c.Name,
Description: c.Description,
Event: expand(c.EventSig, vars),
EmitSig: expand(c.EmitSig, vars),
EmitArg: make([]string, 0, len(c.EmitArgs)),
Args: make([]EventArg, 0, len(c.Args)),
ExpectKey: !strings.EqualFold(strings.TrimSpace(c.Expected), "fail"),
}
for _, a := range c.EmitArgs {
tc.EmitArg = append(tc.EmitArg, expand(a, vars))
}
for _, a := range c.Args {
tc.Args = append(tc.Args, EventArg{
Name: expand(a.Name, vars),
Op: expand(a.Op, vars),
Number: expand(a.Number, vars),
Bytes: expand(a.Bytes, vars),
})
}
out = append(out, tc)
}
return out, nil
}

func expand(s string, vars map[string]string) string {
return varRe.ReplaceAllStringFunc(s, func(m string) string {
sub := varRe.FindStringSubmatch(m)
if len(sub) != 2 {
return m
}
if v, ok := vars[sub[1]]; ok {
return v
}
if v := os.Getenv(sub[1]); v != "" {
return v
}
return m
})
}
Loading