From ca53648a683fd5d3a4672eb80d1916343391a8f9 Mon Sep 17 00:00:00 2001 From: dusan sekulic Date: Fri, 18 Oct 2024 10:56:15 +0200 Subject: [PATCH 1/7] Simulation framework for multi-client server testing This PR introduces a simulation framework designed to test the server by simulating multiple clients performing various actions over several rounds. The framework reads simulation configurations from YAML files, validates them against a predefined schema, and executes the simulation accordingly. Here's a summary of the key changes made: 1. **New `simulation` Module**: - Added a new Go module `simulation` with its own `go.mod` and `go.sum`. - Introduced a main application for running simulations based on a YAML configuration. - Included functions for setting up clients, performing actions like onboarding, sending transactions, and claiming funds. - Added validation of simulation configuration against a schema using `gojsonschema`. 2. **Simulation Configuration**: - Added `simulation1.yaml` as an example configuration file for running simulations. - Defined a `schema.yaml` for validating simulation configurations. 3. **`.gitignore` Updates**: - Added `.idea` to ignore IntelliJ IDEA project files. 4. **`Dockerfile` Update**: - Updated the base image version of Golang from `1.23.1` to `1.23.2`. 5. **`go.work` Update**: - Updated the Go version from `1.23.1` to `1.23.2`. - Added a new directory `./simulation` to the workspace. 6. **`go.work.sum` Update**: - Added new dependencies `github.com/coreos/go-systemd` and `github.com/go-task/slim-sprig`. 7. **`e2e_test.go` Modifications**: - Removed the `setupAspWallet` function and replaced its usage with `utils.SetupAspWalletCovenantless`. 8. **`test_utils.go` Modifications**: - Added a new function `SetupAspWalletCovenantless` to handle wallet setup with optional initial funding. --- .gitignore | 4 +- Dockerfile | 2 +- go.work | 3 +- go.work.sum | 2 + server/test/e2e/covenantless/e2e_test.go | 86 +----- server/test/e2e/test_utils.go | 97 ++++++ simulation/go.mod | 19 ++ simulation/go.sum | 12 + simulation/main.go | 299 ++++++++++++++++++ simulation/schema.yaml | 18 ++ simulation/simulation1.yaml | 372 +++++++++++++++++++++++ 11 files changed, 826 insertions(+), 88 deletions(-) create mode 100644 simulation/go.mod create mode 100644 simulation/go.sum create mode 100644 simulation/main.go create mode 100644 simulation/schema.yaml create mode 100644 simulation/simulation1.yaml diff --git a/.gitignore b/.gitignore index c6e0414c2..94aa02dd9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ .vscode/ *.wasm -wasm_exec.js \ No newline at end of file +wasm_exec.js + +.idea \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 7b3c79b1f..a49aff0e1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # First image used to build the sources -FROM golang:1.23.1 AS builder +FROM golang:1.23.2 AS builder ARG VERSION ARG TARGETOS diff --git a/go.work b/go.work index 4025b5804..0af4ef858 100644 --- a/go.work +++ b/go.work @@ -1,4 +1,4 @@ -go 1.23.1 +go 1.23.2 use ( ./api-spec @@ -8,6 +8,7 @@ use ( ./server ./server/pkg/kvdb ./server/pkg/macaroons + ./simulation ) replace github.com/btcsuite/btcd/btcec/v2 => github.com/btcsuite/btcd/btcec/v2 v2.3.3 diff --git a/go.work.sum b/go.work.sum index 59e2af44f..04ff76acd 100644 --- a/go.work.sum +++ b/go.work.sum @@ -520,6 +520,7 @@ github.com/coreos/go-etcd v2.0.0+incompatible h1:bXhRBIXoTm9BYHS3gE0TtQuyNZyeEMu github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= @@ -575,6 +576,7 @@ github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KE github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobuffalo/here v0.6.0 h1:hYrd0a6gDmWxBM4TnrGw8mQg24iSVoIkHEk7FodQcBI= diff --git a/server/test/e2e/covenantless/e2e_test.go b/server/test/e2e/covenantless/e2e_test.go index 61f51b190..1688651a0 100644 --- a/server/test/e2e/covenantless/e2e_test.go +++ b/server/test/e2e/covenantless/e2e_test.go @@ -1,11 +1,9 @@ package e2e_test import ( - "bytes" "context" "encoding/json" "fmt" - "net/http" "os" "testing" "time" @@ -40,7 +38,7 @@ func TestMain(m *testing.M) { os.Exit(1) } - if err := setupAspWallet(); err != nil { + if err := utils.SetupAspWalletCovenantless(0.0); err != nil { fmt.Println(err) os.Exit(1) } @@ -397,88 +395,6 @@ func runClarkCommand(arg ...string) (string, error) { return utils.RunCommand("docker", args...) } -func setupAspWallet() error { - adminHttpClient := &http.Client{ - Timeout: 15 * time.Second, - } - - req, err := http.NewRequest("GET", "http://localhost:7070/v1/admin/wallet/seed", nil) - if err != nil { - return fmt.Errorf("failed to prepare generate seed request: %s", err) - } - req.Header.Set("Authorization", "Basic YWRtaW46YWRtaW4=") - - seedResp, err := adminHttpClient.Do(req) - if err != nil { - return fmt.Errorf("failed to generate seed: %s", err) - } - - var seed struct { - Seed string `json:"seed"` - } - - if err := json.NewDecoder(seedResp.Body).Decode(&seed); err != nil { - return fmt.Errorf("failed to parse response: %s", err) - } - - reqBody := bytes.NewReader([]byte(fmt.Sprintf(`{"seed": "%s", "password": "%s"}`, seed.Seed, utils.Password))) - req, err = http.NewRequest("POST", "http://localhost:7070/v1/admin/wallet/create", reqBody) - if err != nil { - return fmt.Errorf("failed to prepare wallet create request: %s", err) - } - req.Header.Set("Authorization", "Basic YWRtaW46YWRtaW4=") - req.Header.Set("Content-Type", "application/json") - - if _, err := adminHttpClient.Do(req); err != nil { - return fmt.Errorf("failed to create wallet: %s", err) - } - - reqBody = bytes.NewReader([]byte(fmt.Sprintf(`{"password": "%s"}`, utils.Password))) - req, err = http.NewRequest("POST", "http://localhost:7070/v1/admin/wallet/unlock", reqBody) - if err != nil { - return fmt.Errorf("failed to prepare wallet unlock request: %s", err) - } - req.Header.Set("Authorization", "Basic YWRtaW46YWRtaW4=") - req.Header.Set("Content-Type", "application/json") - - if _, err := adminHttpClient.Do(req); err != nil { - return fmt.Errorf("failed to unlock wallet: %s", err) - } - - time.Sleep(time.Second) - - req, err = http.NewRequest("GET", "http://localhost:7070/v1/admin/wallet/address", nil) - if err != nil { - return fmt.Errorf("failed to prepare new address request: %s", err) - } - req.Header.Set("Authorization", "Basic YWRtaW46YWRtaW4=") - - resp, err := adminHttpClient.Do(req) - if err != nil { - return fmt.Errorf("failed to get new address: %s", err) - } - - var addr struct { - Address string `json:"address"` - } - - if err := json.NewDecoder(resp.Body).Decode(&addr); err != nil { - return fmt.Errorf("failed to parse response: %s", err) - } - - const numberOfFaucet = 15 // must cover the liquidity needed for all tests - - for i := 0; i < numberOfFaucet; i++ { - _, err = utils.RunCommand("nigiri", "faucet", addr.Address) - if err != nil { - return fmt.Errorf("failed to fund wallet: %s", err) - } - } - - time.Sleep(5 * time.Second) - return nil -} - func setupArkSDK(t *testing.T) (arksdk.ArkClient, client.ASPClient) { storeSvc, err := inmemorystore.NewConfigStore() require.NoError(t, err) diff --git a/server/test/e2e/test_utils.go b/server/test/e2e/test_utils.go index c28ba9edc..62b551912 100644 --- a/server/test/e2e/test_utils.go +++ b/server/test/e2e/test_utils.go @@ -1,8 +1,11 @@ package e2e import ( + "bytes" + "encoding/json" "fmt" "io" + "net/http" "os/exec" "strings" "sync" @@ -104,3 +107,97 @@ func newCommand(name string, arg ...string) *exec.Cmd { cmd := exec.Command(name, arg...) return cmd } + +func SetupAspWalletCovenantless(initFunding float64) error { + adminHttpClient := &http.Client{ + Timeout: 15 * time.Second, + } + + req, err := http.NewRequest("GET", "http://localhost:7070/v1/admin/wallet/seed", nil) + if err != nil { + return fmt.Errorf("failed to prepare generate seed request: %s", err) + } + req.Header.Set("Authorization", "Basic YWRtaW46YWRtaW4=") + + seedResp, err := adminHttpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to generate seed: %s", err) + } + + var seed struct { + Seed string `json:"seed"` + } + + if err := json.NewDecoder(seedResp.Body).Decode(&seed); err != nil { + return fmt.Errorf("failed to parse response: %s", err) + } + + reqBody := bytes.NewReader([]byte(fmt.Sprintf(`{"seed": "%s", "password": "%s"}`, seed.Seed, Password))) + req, err = http.NewRequest("POST", "http://localhost:7070/v1/admin/wallet/create", reqBody) + if err != nil { + return fmt.Errorf("failed to prepare wallet create request: %s", err) + } + req.Header.Set("Authorization", "Basic YWRtaW46YWRtaW4=") + req.Header.Set("Content-Type", "application/json") + + if _, err := adminHttpClient.Do(req); err != nil { + return fmt.Errorf("failed to create wallet: %s", err) + } + + reqBody = bytes.NewReader([]byte(fmt.Sprintf(`{"password": "%s"}`, Password))) + req, err = http.NewRequest("POST", "http://localhost:7070/v1/admin/wallet/unlock", reqBody) + if err != nil { + return fmt.Errorf("failed to prepare wallet unlock request: %s", err) + } + req.Header.Set("Authorization", "Basic YWRtaW46YWRtaW4=") + req.Header.Set("Content-Type", "application/json") + + if _, err := adminHttpClient.Do(req); err != nil { + return fmt.Errorf("failed to unlock wallet: %s", err) + } + + time.Sleep(time.Second) + + req, err = http.NewRequest("GET", "http://localhost:7070/v1/admin/wallet/address", nil) + if err != nil { + return fmt.Errorf("failed to prepare new address request: %s", err) + } + req.Header.Set("Authorization", "Basic YWRtaW46YWRtaW4=") + + resp, err := adminHttpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to get new address: %s", err) + } + + var addr struct { + Address string `json:"address"` + } + + if err := json.NewDecoder(resp.Body).Decode(&addr); err != nil { + return fmt.Errorf("failed to parse response: %s", err) + } + + if initFunding == 0.0 { + const numberOfFaucet = 15 // must cover the liquidity needed for all tests + for i := 0; i < numberOfFaucet; i++ { + _, err = RunCommand("nigiri", "faucet", addr.Address) + if err != nil { + return fmt.Errorf("failed to fund wallet: %s", err) + } + } + + } else { + chunkSize := initFunding / 5 + for i := 0; i < 5; i++ { + amount := fmt.Sprintf("%.8f", chunkSize) + _, err = RunCommand("nigiri", "faucet", addr.Address, amount) + if err != nil { + return fmt.Errorf("failed to fund wallet chunk %d: %s", i+1, err) + } + time.Sleep(1 * time.Second) + } + } + + time.Sleep(5 * time.Second) + return nil +} diff --git a/simulation/go.mod b/simulation/go.mod new file mode 100644 index 000000000..f8ac3553f --- /dev/null +++ b/simulation/go.mod @@ -0,0 +1,19 @@ +module github.com/ark-network/ark/simulation + +go 1.23.2 + +require ( + github.com/xeipuuv/gojsonschema v1.2.0 + gopkg.in/yaml.v2 v2.4.0 +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/stretchr/testify v1.9.0 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect +) diff --git a/simulation/go.sum b/simulation/go.sum new file mode 100644 index 000000000..7ffdeedf8 --- /dev/null +++ b/simulation/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/simulation/main.go b/simulation/main.go new file mode 100644 index 000000000..c667cf6a4 --- /dev/null +++ b/simulation/main.go @@ -0,0 +1,299 @@ +package main + +import ( + "context" + "fmt" + "os" + "sync" + "time" + + arksdk "github.com/ark-network/ark/pkg/client-sdk" + inmemorystore "github.com/ark-network/ark/pkg/client-sdk/store/inmemory" + utils "github.com/ark-network/ark/server/test/e2e" + log "github.com/sirupsen/logrus" + + "github.com/xeipuuv/gojsonschema" + "sigs.k8s.io/yaml" +) + +const ( + composePath = "../docker-compose.clark.regtest.yml" +) + +var ( + aspUrl = "localhost:7070" + clientType = arksdk.GrpcClient + password = "password" + walletType = arksdk.SingleKeyWallet +) + +func main() { + simulation, err := loadAndValidateSimulation() + if err != nil { + log.Fatal(err) + } + + log.Infof("Simulation Version: %s\n", simulation.Version) + log.Infof("ASP Network: %s\n", simulation.ASP.Network) + log.Infof("Number of Clients: %d\n", len(simulation.Clients)) + log.Infof("Number of Rounds: %d\n", len(simulation.Rounds)) + + roundLifetime := fmt.Sprintf("%d", simulation.ASP.RoundInterval) + tmpfile, err := os.CreateTemp("", "docker-env") + if err != nil { + log.Fatal(err) + } + defer os.Remove(tmpfile.Name()) // clean up + + if _, err := tmpfile.Write([]byte(roundLifetime)); err != nil { + log.Fatal(err) + } + if err := tmpfile.Close(); err != nil { + log.Fatal(err) + } + + log.Infof("start building ARKD docker container ...") + if _, err := utils.RunCommand("docker", "compose", "-f", composePath, "--env-file", tmpfile.Name(), "up", "-d", "--build"); err != nil { + log.Fatal(err) + } + + time.Sleep(10 * time.Second) + log.Infoln("ASP running...") + + if err := utils.SetupAspWalletCovenantless(simulation.ASP.InitialFunding); err != nil { + log.Fatal(err) + } + + time.Sleep(3 * time.Second) + + log.Infoln("ASP wallet initialized") + + go func() { + if err := utils.GenerateBlock(); err != nil { + log.Fatal(err) + } + + time.Sleep(5 * time.Second) + }() + + users := make(map[string]User) + usersMtx := sync.Mutex{} + var wg sync.WaitGroup + for _, v := range simulation.Clients { + wg.Add(1) + go func(id string, initialFunding float64) { + defer wg.Done() + cl, err := setupArkClient() + if err != nil { + log.Fatal(err) + } + + usersMtx.Lock() + users[id] = User{ + aspClient: cl, + ID: id, + InitialFunding: initialFunding, + } + usersMtx.Unlock() + }(v.ID, v.InitialFunding) + } + wg.Wait() + + log.Infof("client wallets initialized") + + for _, round := range simulation.Rounds { + log.Infof("Executing Round %d\n", round.Number) + var wg sync.WaitGroup + for clientID, actions := range round.Actions { + user, ok := users[clientID] + if !ok { + log.Fatalf("User %s not found", clientID) + } + for _, action := range actions { + wg.Add(1) + go func(u User, a interface{}) { + defer wg.Done() + actionMap := a.(map[string]interface{}) + actionType := actionMap["type"].(string) + amount, _ := actionMap["amount"].(float64) + to, _ := actionMap["to"].(string) + + var err error + switch actionType { + case "Onboard": + err = onboard(u, amount) + case "SendAsync": + err = sendAsync(u, amount, to, users) + case "Claim": + err = claim(u) + default: + log.Printf("Unknown action type: %s for user %s", actionType, u.ID) + return + } + + if err != nil { + log.Printf("%s failed for user %s: %v", actionType, u.ID, err) + } + }(user, action) + } + } + wg.Wait() + time.Sleep(time.Duration(simulation.ASP.RoundInterval) * 2 * time.Second) + } + +} + +func loadAndValidateSimulation() (*Simulation, error) { + schemaBytes, err := os.ReadFile("schema.yaml") + if err != nil { + return nil, fmt.Errorf("error reading schema file: %v", err) + } + + schemaJSON, err := yaml.YAMLToJSON(schemaBytes) + if err != nil { + return nil, fmt.Errorf("error converting schema YAML to JSON: %v", err) + } + + schemaLoader := gojsonschema.NewBytesLoader(schemaJSON) + + simBytes, err := os.ReadFile("simulation1.yaml") + if err != nil { + return nil, fmt.Errorf("error reading simulation file: %v", err) + } + + var sim Simulation + err = yaml.Unmarshal(simBytes, &sim) + if err != nil { + return nil, fmt.Errorf("error parsing simulation YAML: %v", err) + } + + simJSON, err := yaml.YAMLToJSON(simBytes) + if err != nil { + return nil, fmt.Errorf("error converting simulation YAML to JSON: %v", err) + } + + documentLoader := gojsonschema.NewBytesLoader(simJSON) + + result, err := gojsonschema.Validate(schemaLoader, documentLoader) + if err != nil { + return nil, fmt.Errorf("error validating simulation: %v", err) + } + + if !result.Valid() { + // Collect error messages + var errorMessages string + for _, desc := range result.Errors() { + errorMessages += fmt.Sprintf("- %s\n", desc) + } + return nil, fmt.Errorf("the simulation is not valid:\n%s", errorMessages) + } + + return &sim, nil +} + +func setupArkClient() (arksdk.ArkClient, error) { + storeSvc, err := inmemorystore.NewConfigStore() + if err != nil { + return nil, fmt.Errorf("failed to setup store: %s", err) + } + client, err := arksdk.NewCovenantlessClient(storeSvc) + if err != nil { + return nil, fmt.Errorf("failed to setup ark client: %s", err) + } + + if err := client.Init(context.Background(), arksdk.InitArgs{ + WalletType: walletType, + ClientType: clientType, + AspUrl: aspUrl, + Password: password, + }); err != nil { + return nil, fmt.Errorf("failed to initialize wallet: %s", err) + } + + return client, nil +} + +func onboard(user User, amount float64) error { + ctx := context.Background() + if err := user.aspClient.Unlock(ctx, password); err != nil { + return err + } + defer user.aspClient.Lock(ctx, password) + + _, boardingAddress, err := user.aspClient.Receive(ctx) + if err != nil { + return err + } + + amountStr := fmt.Sprintf("%.8f", amount) + + if _, err := utils.RunCommand("nigiri", "faucet", boardingAddress, amountStr); err != nil { + return err + } + + time.Sleep(2 * time.Second) + + _, err = user.aspClient.Claim(ctx) + return err +} + +func sendAsync(user User, amount float64, to string, users map[string]User) error { + ctx := context.Background() + if err := user.aspClient.Unlock(ctx, password); err != nil { + return err + } + defer user.aspClient.Lock(ctx, password) + + toUser, ok := users[to] + if !ok { + return fmt.Errorf("recipient user %s not found", to) + } + + toAddress, _, err := toUser.aspClient.Receive(ctx) + if err != nil { + return err + } + + receivers := []arksdk.Receiver{ + arksdk.NewBitcoinReceiver(toAddress, uint64(amount*1e8)), + } + + _, err = user.aspClient.SendAsync(ctx, false, receivers) + return err +} + +func claim(user User) error { + ctx := context.Background() + if err := user.aspClient.Unlock(ctx, password); err != nil { + return err + } + defer user.aspClient.Lock(ctx, password) + + _, err := user.aspClient.Claim(ctx) + return err +} + +type Simulation struct { + Version string `yaml:"version"` + ASP struct { + Network string `yaml:"network"` + RoundInterval int `yaml:"round_interval"` + InitialFunding float64 `yaml:"initial_funding"` + } `yaml:"asp"` + Clients []struct { + ID string `yaml:"id"` + Name string `yaml:"name"` + InitialFunding float64 `yaml:"initial_funding,omitempty"` + } `yaml:"clients"` + Rounds []struct { + Number int `yaml:"number"` + Actions map[string][]interface{} `yaml:"actions"` + } `yaml:"rounds"` +} + +type User struct { + aspClient arksdk.ArkClient + ID string + Name string + InitialFunding float64 +} diff --git a/simulation/schema.yaml b/simulation/schema.yaml new file mode 100644 index 000000000..09fad3d43 --- /dev/null +++ b/simulation/schema.yaml @@ -0,0 +1,18 @@ +version: string() + +asp: + network: enum('regtest', 'testnet', 'mainnet') + initial_funding: float(min=0) + round_interval: int(min=1) + +clients: + - id: string() + initial_funding: float(min=0, required=false) + +rounds: + - number: int(min=1) + actions: + regex('^client_\d+$'): + - type: enum('Onboard', 'SendAsync', 'SendOffchain', 'SendOnchain', 'Claim', 'CollaborativeExit', 'UnilateralExit') + amount: float(min=0, required=false) + to: string(required=false) \ No newline at end of file diff --git a/simulation/simulation1.yaml b/simulation/simulation1.yaml new file mode 100644 index 000000000..a2cec5828 --- /dev/null +++ b/simulation/simulation1.yaml @@ -0,0 +1,372 @@ +version: "1.0" + +asp: + network: "regtest" + initial_funding: 1.0 + round_interval: 20 + +clients: + - id: "client_0" + initial_funding: 0.1 + - id: "client_1" + name: "Client1" + - id: "client_2" + name: "Client2" + - id: "client_3" + name: "Client3" + - id: "client_4" + name: "Client4" + - id: "client_5" + name: "Client5" + - id: "client_6" + name: "Client6" + - id: "client_7" + name: "Client7" + - id: "client_8" + name: "Client8" + - id: "client_9" + name: "Client9" + - id: "client_10" + name: "Client10" + - id: "client_11" + name: "Client11" + - id: "client_12" + name: "Client12" + - id: "client_13" + name: "Client13" + - id: "client_14" + name: "Client14" + - id: "client_15" + name: "Client15" + - id: "client_16" + name: "Client16" + - id: "client_17" + name: "Client17" + - id: "client_18" + name: "Client18" + - id: "client_19" + name: "Client19" + - id: "client_20" + name: "Client20" + - id: "client_21" + name: "Client21" + - id: "client_22" + name: "Client22" + - id: "client_23" + name: "Client23" + - id: "client_24" + name: "Client24" + - id: "client_25" + name: "Client25" + - id: "client_26" + name: "Client26" + - id: "client_27" + name: "Client27" + - id: "client_28" + name: "Client28" + - id: "client_29" + name: "Client29" + - id: "client_30" + name: "Client30" + - id: "client_31" + name: "Client31" + - id: "client_32" + name: "Client32" + - id: "client_33" + name: "Client33" + - id: "client_34" + name: "Client34" + - id: "client_35" + name: "Client35" + - id: "client_36" + name: "Client36" + - id: "client_37" + name: "Client37" + - id: "client_38" + name: "Client38" + - id: "client_39" + name: "Client39" + - id: "client_40" + name: "Client40" + - id: "client_41" + name: "Client41" + - id: "client_42" + name: "Client42" + - id: "client_43" + name: "Client43" + - id: "client_44" + name: "Client44" + - id: "client_45" + name: "Client45" + - id: "client_46" + name: "Client46" + - id: "client_47" + name: "Client47" + - id: "client_48" + name: "Client48" + - id: "client_49" + name: "Client49" + - id: "client_50" + name: "Client50" + +rounds: + - number: 1 + actions: + client_0: + - type: "Onboard" + amount: 0.1 + - number: 2 + actions: + client_0: + - type: "SendAsync" + amount: 0.002 + to: "client_1" + - type: "SendAsync" + amount: 0.002 + to: "client_2" + - type: "SendAsync" + amount: 0.002 + to: "client_3" + - type: "SendAsync" + amount: 0.002 + to: "client_4" + - type: "SendAsync" + amount: 0.002 + to: "client_5" + - type: "SendAsync" + amount: 0.002 + to: "client_6" + - type: "SendAsync" + amount: 0.002 + to: "client_7" + - type: "SendAsync" + amount: 0.002 + to: "client_8" + - type: "SendAsync" + amount: 0.002 + to: "client_9" + - type: "SendAsync" + amount: 0.002 + to: "client_10" + - type: "SendAsync" + amount: 0.002 + to: "client_11" + - type: "SendAsync" + amount: 0.002 + to: "client_12" + - type: "SendAsync" + amount: 0.002 + to: "client_13" + - type: "SendAsync" + amount: 0.002 + to: "client_14" + - type: "SendAsync" + amount: 0.002 + to: "client_15" + - type: "SendAsync" + amount: 0.002 + to: "client_16" + - type: "SendAsync" + amount: 0.002 + to: "client_17" + - type: "SendAsync" + amount: 0.002 + to: "client_18" + - type: "SendAsync" + amount: 0.002 + to: "client_19" + - type: "SendAsync" + amount: 0.002 + to: "client_20" + - type: "SendAsync" + amount: 0.002 + to: "client_21" + - type: "SendAsync" + amount: 0.002 + to: "client_22" + - type: "SendAsync" + amount: 0.002 + to: "client_23" + - type: "SendAsync" + amount: 0.002 + to: "client_24" + - type: "SendAsync" + amount: 0.002 + to: "client_25" + - type: "SendAsync" + amount: 0.002 + to: "client_26" + - type: "SendAsync" + amount: 0.002 + to: "client_27" + - type: "SendAsync" + amount: 0.002 + to: "client_28" + - type: "SendAsync" + amount: 0.002 + to: "client_29" + - type: "SendAsync" + amount: 0.002 + to: "client_30" + - type: "SendAsync" + amount: 0.002 + to: "client_31" + - type: "SendAsync" + amount: 0.002 + to: "client_32" + - type: "SendAsync" + amount: 0.002 + to: "client_33" + - type: "SendAsync" + amount: 0.002 + to: "client_34" + - type: "SendAsync" + amount: 0.002 + to: "client_35" + - type: "SendAsync" + amount: 0.002 + to: "client_36" + - type: "SendAsync" + amount: 0.002 + to: "client_37" + - type: "SendAsync" + amount: 0.002 + to: "client_38" + - type: "SendAsync" + amount: 0.002 + to: "client_39" + - type: "SendAsync" + amount: 0.002 + to: "client_40" + - type: "SendAsync" + amount: 0.002 + to: "client_41" + - type: "SendAsync" + amount: 0.002 + to: "client_42" + - type: "SendAsync" + amount: 0.002 + to: "client_43" + - type: "SendAsync" + amount: 0.002 + to: "client_44" + - type: "SendAsync" + amount: 0.002 + to: "client_45" + - type: "SendAsync" + amount: 0.002 + to: "client_46" + - type: "SendAsync" + amount: 0.002 + to: "client_47" + - type: "SendAsync" + amount: 0.002 + to: "client_48" + - type: "SendAsync" + amount: 0.002 + to: "client_49" + - type: "SendAsync" + amount: 0.002 + to: "client_50" + - number: 3 + actions: + client_1: + - type: "Claim" + client_2: + - type: "Claim" + client_3: + - type: "Claim" + client_4: + - type: "Claim" + client_5: + - type: "Claim" + client_6: + - type: "Claim" + client_7: + - type: "Claim" + client_8: + - type: "Claim" + client_9: + - type: "Claim" + client_10: + - type: "Claim" + client_11: + - type: "Claim" + client_12: + - type: "Claim" + client_13: + - type: "Claim" + client_14: + - type: "Claim" + client_15: + - type: "Claim" + client_16: + - type: "Claim" + client_17: + - type: "Claim" + client_18: + - type: "Claim" + client_19: + - type: "Claim" + client_20: + - type: "Claim" + client_21: + - type: "Claim" + client_22: + - type: "Claim" + client_23: + - type: "Claim" + client_24: + - type: "Claim" + client_25: + - type: "Claim" + client_26: + - type: "Claim" + client_27: + - type: "Claim" + client_28: + - type: "Claim" + client_29: + - type: "Claim" + client_30: + - type: "Claim" + client_31: + - type: "Claim" + client_32: + - type: "Claim" + client_33: + - type: "Claim" + client_34: + - type: "Claim" + client_35: + - type: "Claim" + client_36: + - type: "Claim" + client_37: + - type: "Claim" + client_38: + - type: "Claim" + client_39: + - type: "Claim" + client_40: + - type: "Claim" + client_41: + - type: "Claim" + client_42: + - type: "Claim" + client_43: + - type: "Claim" + client_44: + - type: "Claim" + client_45: + - type: "Claim" + client_46: + - type: "Claim" + client_47: + - type: "Claim" + client_48: + - type: "Claim" + client_49: + - type: "Claim" + client_50: + - type: "Claim" \ No newline at end of file From 056b396c3c3dc1bb9adb0b9b9d8ea9115bc13751 Mon Sep 17 00:00:00 2001 From: dusan sekulic Date: Fri, 18 Oct 2024 12:33:01 +0200 Subject: [PATCH 2/7] go work sync --- simulation/go.mod | 5 ++++- simulation/go.sum | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/simulation/go.mod b/simulation/go.mod index f8ac3553f..3427d6f3d 100644 --- a/simulation/go.mod +++ b/simulation/go.mod @@ -3,17 +3,20 @@ module github.com/ark-network/ark/simulation go 1.23.2 require ( + github.com/sirupsen/logrus v1.9.3 github.com/xeipuuv/gojsonschema v1.2.0 - gopkg.in/yaml.v2 v2.4.0 + sigs.k8s.io/yaml v1.4.0 ) require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/stretchr/testify v1.9.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + golang.org/x/sys v0.24.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/simulation/go.sum b/simulation/go.sum index 7ffdeedf8..225cc34b1 100644 --- a/simulation/go.sum +++ b/simulation/go.sum @@ -1,12 +1,15 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= From ee4e34b80dc60b0744b1ad46bcae533698fe735f Mon Sep 17 00:00:00 2001 From: dusan sekulic Date: Fri, 18 Oct 2024 12:37:51 +0200 Subject: [PATCH 3/7] pr review refactor --- Dockerfile | 2 +- go.work | 2 +- simulation/go.mod | 2 +- simulation/main.go | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index a49aff0e1..7b3c79b1f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # First image used to build the sources -FROM golang:1.23.2 AS builder +FROM golang:1.23.1 AS builder ARG VERSION ARG TARGETOS diff --git a/go.work b/go.work index 0af4ef858..134934248 100644 --- a/go.work +++ b/go.work @@ -1,4 +1,4 @@ -go 1.23.2 +go 1.23.1 use ( ./api-spec diff --git a/simulation/go.mod b/simulation/go.mod index 3427d6f3d..e3fc21ec2 100644 --- a/simulation/go.mod +++ b/simulation/go.mod @@ -1,6 +1,6 @@ module github.com/ark-network/ark/simulation -go 1.23.2 +go 1.23.1 require ( github.com/sirupsen/logrus v1.9.3 diff --git a/simulation/main.go b/simulation/main.go index a91d56716..ebbe37deb 100644 --- a/simulation/main.go +++ b/simulation/main.go @@ -60,7 +60,7 @@ func main() { log.Fatal(err) } - log.Infof("start building ARKD docker container ...") + log.Infof("Start building ARKD docker container ...") if _, err := utils.RunCommand("docker", "compose", "-f", composePath, "--env-file", tmpfile.Name(), "up", "-d", "--build"); err != nil { log.Fatal(err) } @@ -117,7 +117,7 @@ func main() { } wg.Wait() - log.Infof("client wallets initialized") + log.Infof("Client wallets initialized") for _, round := range simulation.Rounds { log.Infof("Executing Round %d\n", round.Number) From 8e9e6899cb427ce73a6aad9d72e10325b6fc220b Mon Sep 17 00:00:00 2001 From: dusan sekulic Date: Fri, 18 Oct 2024 13:39:30 +0200 Subject: [PATCH 4/7] fix readme --- simulation/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simulation/README.md b/simulation/README.md index 5540206d0..f11a1e534 100644 --- a/simulation/README.md +++ b/simulation/README.md @@ -1,6 +1,6 @@ # Simulation Framework for Server Testing -This simulation framework is designed to test the **ASP (Ark Server Provider)** by simulating multiple clients performing +This simulation framework is designed to test the **Ark Server** by simulating multiple clients performing various actions over several rounds. It reads simulation configurations from YAML files, validates them against a predefined schema, and executes the simulation accordingly. From 770c0e85c16c97d501ad12c1bb96d04f7a263c27 Mon Sep 17 00:00:00 2001 From: dusan sekulic Date: Fri, 18 Oct 2024 16:36:23 +0200 Subject: [PATCH 5/7] pr review refactor --- simulation/main.go | 37 +++++++------------------------------ 1 file changed, 7 insertions(+), 30 deletions(-) diff --git a/simulation/main.go b/simulation/main.go index ebbe37deb..5607b01dc 100644 --- a/simulation/main.go +++ b/simulation/main.go @@ -27,9 +27,6 @@ var ( clientType = arksdk.GrpcClient password = "password" walletType = arksdk.SingleKeyWallet - - tempDirs []string - tempDirsMutex sync.Mutex ) func main() { @@ -46,7 +43,7 @@ func main() { log.Infof("Number of Clients: %d\n", len(simulation.Clients)) log.Infof("Number of Rounds: %d\n", len(simulation.Rounds)) - roundLifetime := fmt.Sprintf("%d", simulation.Server.RoundInterval) + roundLifetime := fmt.Sprintf("ARK_ROUND_INTERVAL=%d", simulation.Server.RoundInterval) tmpfile, err := os.CreateTemp("", "docker-env") if err != nil { log.Fatal(err) @@ -77,20 +74,12 @@ func main() { log.Infoln("ASP wallet initialized") go func() { - if err := utils.GenerateBlock(); err != nil { - log.Fatal(err) - } - - time.Sleep(5 * time.Second) - }() - - defer func() { - tempDirsMutex.Lock() - defer tempDirsMutex.Unlock() - for _, dir := range tempDirs { - if err := os.RemoveAll(dir); err != nil { - log.Errorf("failed to remove dir: %v", err) + for { + if err := utils.GenerateBlock(); err != nil { + log.Fatal(err) } + + time.Sleep(5 * time.Second) } }() @@ -210,20 +199,8 @@ func loadAndValidateSimulation(simFile string) (*Simulation, error) { } func setupArkClient() (arksdk.ArkClient, error) { - tempDir, err := os.MkdirTemp("", "ark_client_*") - if err != nil { - return nil, fmt.Errorf("failed to create temporary directory: %s", err) - } - - // Store the temporary directory path for later cleanup - tempDirsMutex.Lock() - tempDirs = append(tempDirs, tempDir) - tempDirsMutex.Unlock() - appDataStore, err := store.NewStore(store.Config{ - ConfigStoreType: types.FileStore, - AppDataStoreType: types.KVStore, - BaseDir: tempDir, + ConfigStoreType: types.InMemoryStore, }) if err != nil { From 716ab7dd24f79d8c6ce605721f3db3bbf78e2c9d Mon Sep 17 00:00:00 2001 From: dusan sekulic Date: Mon, 21 Oct 2024 13:21:46 +0200 Subject: [PATCH 6/7] fixes --- docker-compose.regtest.yml | 26 +++---- simulation/main.go | 139 ++++++++++++++++++++++++------------ simulation/schema.yaml | 94 +++++++++++++++++++----- simulation/simulation1.yaml | 2 +- 4 files changed, 182 insertions(+), 79 deletions(-) diff --git a/docker-compose.regtest.yml b/docker-compose.regtest.yml index 834da4341..aad150abc 100644 --- a/docker-compose.regtest.yml +++ b/docker-compose.regtest.yml @@ -27,19 +27,19 @@ services: depends_on: - oceand environment: - - ARK_WALLET_ADDR=oceand:18000 - - ARK_ROUND_INTERVAL=10 - - ARK_NETWORK=liquidregtest - - ARK_LOG_LEVEL=5 - - ARK_ESPLORA_URL=http://chopsticks-liquid:3000 - - ARK_ROUND_LIFETIME=20 - - ARK_SCHEDULER_TYPE=block - - ARK_DB_TYPE=sqlite - - ARK_TX_BUILDER_TYPE=covenant - - ARK_PORT=6060 - - ARK_NO_TLS=true - - ARK_NO_MACAROONS=true - - ARK_DATADIR=/app/data + - ARK_WALLET_ADDR=${ARK_WALLET_ADDR:-oceand:18000} + - ARK_ROUND_INTERVAL=${ARK_ROUND_INTERVAL:-10} + - ARK_NETWORK=${ARK_NETWORK:-liquidregtest} + - ARK_LOG_LEVEL=${ARK_LOG_LEVEL:-5} + - ARK_ESPLORA_URL=${ARK_ESPLORA_URL:-http://chopsticks-liquid:3000} + - ARK_ROUND_LIFETIME=${ARK_ROUND_LIFETIME:-20} + - ARK_SCHEDULER_TYPE=${ARK_SCHEDULER_TYPE:-block} + - ARK_DB_TYPE=${ARK_DB_TYPE:-sqlite} + - ARK_TX_BUILDER_TYPE=${ARK_TX_BUILDER_TYPE:-covenant} + - ARK_PORT=${ARK_PORT:-6060} + - ARK_NO_TLS=${ARK_NO_TLS:-true} + - ARK_NO_MACAROONS=${ARK_NO_MACAROONS:-true} + - ARK_DATADIR=${ARK_DATADIR:-/app/data} ports: - "6060:6060" volumes: diff --git a/simulation/main.go b/simulation/main.go index 5607b01dc..f1e85c1bb 100644 --- a/simulation/main.go +++ b/simulation/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/json" "flag" "fmt" "github.com/ark-network/ark/pkg/client-sdk/store" @@ -108,7 +109,7 @@ func main() { log.Infof("Client wallets initialized") - for _, round := range simulation.Rounds { + for i, round := range simulation.Rounds { log.Infof("Executing Round %d\n", round.Number) var wg sync.WaitGroup for clientID, actions := range round.Actions { @@ -116,10 +117,10 @@ func main() { if !ok { log.Fatalf("User %s not found", clientID) } - for _, action := range actions { - wg.Add(1) - go func(u User, a interface{}) { - defer wg.Done() + wg.Add(1) + go func(u User, actions []interface{}) { + defer wg.Done() + for _, a := range actions { actionMap := a.(map[string]interface{}) actionType := actionMap["type"].(string) amount, _ := actionMap["amount"].(float64) @@ -141,16 +142,36 @@ func main() { if err != nil { log.Printf("%s failed for user %s: %v", actionType, u.ID, err) } - }(user, action) - } + } + }(user, actions) } wg.Wait() - time.Sleep(time.Duration(simulation.Server.RoundInterval) * 2 * time.Second) + if i < len(simulation.Rounds)-1 { + time.Sleep(time.Duration(simulation.Server.RoundInterval) * 2 * time.Second) + } } + log.Println("Final balances for all clients:") + for _, user := range users { + wg.Add(1) + go func(u User) { + defer wg.Done() + ctx := context.Background() + balance, err := getBalance(u.client, ctx) + if err != nil { + log.Errorf("Failed to get balance for user %s: %v", u.ID, err) + } else { + if err := printJSON(u.Name, balance); err != nil { + log.Errorf("Failed to print JSON for user %s: %v", u.ID, err) + } + } + }(user) + } + wg.Wait() } func loadAndValidateSimulation(simFile string) (*Simulation, error) { + // Read and convert the schema YAML file to JSON schemaBytes, err := os.ReadFile("schema.yaml") if err != nil { return nil, fmt.Errorf("error reading schema file: %v", err) @@ -161,26 +182,22 @@ func loadAndValidateSimulation(simFile string) (*Simulation, error) { return nil, fmt.Errorf("error converting schema YAML to JSON: %v", err) } - schemaLoader := gojsonschema.NewBytesLoader(schemaJSON) - + // Read and convert the simulation YAML file to JSON simBytes, err := os.ReadFile(simFile) if err != nil { return nil, fmt.Errorf("error reading simulation file: %v", err) } - var sim Simulation - err = yaml.Unmarshal(simBytes, &sim) - if err != nil { - return nil, fmt.Errorf("error parsing simulation YAML: %v", err) - } - simJSON, err := yaml.YAMLToJSON(simBytes) if err != nil { return nil, fmt.Errorf("error converting simulation YAML to JSON: %v", err) } + // Create JSON loaders for the schema and the document + schemaLoader := gojsonschema.NewBytesLoader(schemaJSON) documentLoader := gojsonschema.NewBytesLoader(simJSON) + // Validate the simulation JSON against the schema JSON result, err := gojsonschema.Validate(schemaLoader, documentLoader) if err != nil { return nil, fmt.Errorf("error validating simulation: %v", err) @@ -195,6 +212,13 @@ func loadAndValidateSimulation(simFile string) (*Simulation, error) { return nil, fmt.Errorf("the simulation is not valid:\n%s", errorMessages) } + // Unmarshal the simulation YAML into the Simulation struct + var sim Simulation + err = json.Unmarshal(simJSON, &sim) + if err != nil { + return nil, fmt.Errorf("error parsing simulation YAML: %v", err) + } + return &sim, nil } @@ -212,7 +236,8 @@ func setupArkClient() (arksdk.ArkClient, error) { return nil, fmt.Errorf("failed to setup ark client: %s", err) } - if err := client.Init(context.Background(), arksdk.InitArgs{ + ctx := context.Background() + if err := client.Init(ctx, arksdk.InitArgs{ WalletType: walletType, ClientType: clientType, AspUrl: aspUrl, @@ -221,15 +246,15 @@ func setupArkClient() (arksdk.ArkClient, error) { return nil, fmt.Errorf("failed to initialize wallet: %s", err) } + if err := client.Unlock(ctx, password); err != nil { + return nil, fmt.Errorf("failed to unlock wallet: %s", err) + } + return client, nil } func onboard(user User, amount float64) error { ctx := context.Background() - if err := user.client.Unlock(ctx, password); err != nil { - return err - } - defer user.client.Lock(ctx, password) _, boardingAddress, err := user.client.Receive(ctx) if err != nil { @@ -244,16 +269,17 @@ func onboard(user User, amount float64) error { time.Sleep(2 * time.Second) - _, err = user.client.Claim(ctx) - return err + if _, err = user.client.Claim(ctx); err != nil { + return fmt.Errorf("user %s failed to onboard: %v", user.ID, err) + } + + log.Infof("%s onboarded successfully with %f BTC", user.ID, amount) + + return nil } func sendAsync(user User, amount float64, to string, users map[string]User) error { ctx := context.Background() - if err := user.client.Unlock(ctx, password); err != nil { - return err - } - defer user.client.Lock(ctx, password) toUser, ok := users[to] if !ok { @@ -269,42 +295,61 @@ func sendAsync(user User, amount float64, to string, users map[string]User) erro arksdk.NewBitcoinReceiver(toAddress, uint64(amount*1e8)), } - _, err = user.client.SendAsync(ctx, false, receivers) - return err + if _, err = user.client.SendAsync(ctx, false, receivers); err != nil { + return fmt.Errorf("user %s failed to send %f BTC to user %s: %v", user.ID, amount, toUser.ID, err) + } + + log.Infof("user %s sent %f BTC to user %s", user.ID, amount, toUser.ID) + + return nil } func claim(user User) error { ctx := context.Background() - if err := user.client.Unlock(ctx, password); err != nil { - return err + + if _, err := user.client.Claim(ctx); err != nil { + return fmt.Errorf("user %s failed to claim their funds: %v", user.ID, err) } - defer user.client.Lock(ctx, password) - _, err := user.client.Claim(ctx) - return err + log.Infof("User %s claimed their funds", user.ID) + + return nil +} + +func getBalance(client arksdk.ArkClient, ctx context.Context) (*arksdk.Balance, error) { + return client.Balance(ctx, false) } type Simulation struct { - Version string `yaml:"version"` + Version string `json:"version"` Server struct { - Network string `yaml:"network"` - RoundInterval int `yaml:"round_interval"` - InitialFunding float64 `yaml:"initial_funding"` - } `yaml:"server"` + Network string `json:"network"` + InitialFunding float64 `json:"initial_funding"` + RoundInterval int `json:"round_interval"` + } `json:"server"` Clients []struct { - ID string `yaml:"id"` - Name string `yaml:"name"` - InitialFunding float64 `yaml:"initial_funding,omitempty"` - } `yaml:"clients"` + ID string `json:"id"` + Name string `json:"name"` + InitialFunding float64 `json:"initial_funding,omitempty"` + } `json:"clients"` Rounds []struct { - Number int `yaml:"number"` - Actions map[string][]interface{} `yaml:"actions"` - } `yaml:"rounds"` + Number int `json:"number"` + Actions map[string][]interface{} `json:"actions"` + } `json:"rounds"` } - type User struct { client arksdk.ArkClient ID string Name string InitialFunding float64 } + +func printJSON(user string, resp interface{}) error { + jsonBytes, err := json.MarshalIndent(resp, "", "\t") + if err != nil { + return err + } + log.Infof("User: %v", user) + log.Infoln(string(jsonBytes)) + return nil +} diff --git a/simulation/schema.yaml b/simulation/schema.yaml index 8ffc5b5f0..69ec561de 100644 --- a/simulation/schema.yaml +++ b/simulation/schema.yaml @@ -1,18 +1,76 @@ -version: string() - -server: - network: enum('regtest', 'testnet', 'mainnet') - initial_funding: float(min=0) - round_interval: int(min=1) - -clients: - - id: string() - initial_funding: float(min=0, required=false) - -rounds: - - number: int(min=1) - actions: - regex('^client_\d+$'): - - type: enum('Onboard', 'SendAsync', 'SendOffchain', 'SendOnchain', 'Claim', 'CollaborativeExit', 'UnilateralExit') - amount: float(min=0, required=false) - to: string(required=false) \ No newline at end of file +$schema: "http://json-schema.org/draft-07/schema#" +type: object +required: + - version + - server + - clients + - rounds +properties: + version: + type: string + server: + type: object + required: + - network + - initial_funding + - round_interval + properties: + network: + type: string + enum: ["regtest", "testnet"] + initial_funding: + type: number + minimum: 0 + round_interval: + type: integer + minimum: 1 + clients: + type: array + items: + type: object + required: + - id + properties: + id: + type: string + name: + type: string + initial_funding: + type: number + minimum: 0 + rounds: + type: array + items: + type: object + required: + - number + - actions + properties: + number: + type: integer + minimum: 1 + actions: + type: object + patternProperties: + "^client_\\d+$": + type: array + items: + type: object + required: + - type + properties: + type: + type: string + enum: + - "Onboard" + - "SendAsync" + - "SendOffchain" + - "SendOnchain" + - "Claim" + - "CollaborativeRedeem" + - "UnilateralExit" + amount: + type: number + minimum: 0 + to: + type: string diff --git a/simulation/simulation1.yaml b/simulation/simulation1.yaml index a2cec5828..07c2d8290 100644 --- a/simulation/simulation1.yaml +++ b/simulation/simulation1.yaml @@ -1,6 +1,6 @@ version: "1.0" -asp: +server: network: "regtest" initial_funding: 1.0 round_interval: 20 From e9c943a42c18cd0167e567171666971f757d46ba Mon Sep 17 00:00:00 2001 From: dusan sekulic Date: Mon, 21 Oct 2024 14:12:21 +0200 Subject: [PATCH 7/7] fix --- simulation/simulation1.yaml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/simulation/simulation1.yaml b/simulation/simulation1.yaml index 07c2d8290..df36b9c5d 100644 --- a/simulation/simulation1.yaml +++ b/simulation/simulation1.yaml @@ -106,8 +106,6 @@ clients: name: "Client48" - id: "client_49" name: "Client49" - - id: "client_50" - name: "Client50" rounds: - number: 1 @@ -265,9 +263,6 @@ rounds: - type: "SendAsync" amount: 0.002 to: "client_49" - - type: "SendAsync" - amount: 0.002 - to: "client_50" - number: 3 actions: client_1: @@ -367,6 +362,4 @@ rounds: client_48: - type: "Claim" client_49: - - type: "Claim" - client_50: - type: "Claim" \ No newline at end of file