diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3894a8ad..314573d6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,6 +19,8 @@ jobs: env: GOPRIVATE: github.com/loredanacirstea* GH_ACCESS_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }} + KAYROS_BASE_URL: ${{ secrets.KAYROS_BASE_URL }} + KAYROS_API_KEY: ${{ secrets.KAYROS_API_KEY }} steps: - name: Checkout code @@ -61,3 +63,9 @@ jobs: go test -failfast=false -timeout 2000s -v ./tests/vmhttp -wasm-runtime=wazero go test -failfast=false -timeout 2000s -v ./tests/vmemail -wasm-runtime=wazero # go test -failfast=false -timeout 2000s -v ./tests/vmpostgresql -wasm-runtime=wazero + + if [ -n "${KAYROS_BASE_URL}" ]; then + go test -failfast=false -timeout 3000s -v -run KeeperTestSuite/TestWasmxSimpleStorage ./tests/wasmx -wasm-runtime=wazero -kayros_user_key="${KAYROS_API_KEY}" -kayros_base_url="${KAYROS_BASE_URL}" -consensus-label=consensus_kayrosp2p_ondemand_0.0.1 + else + echo "Skipping Kayros test (KAYROS_BASE_URL not set)" + fi diff --git a/.gitignore b/.gitignore index 876c1b9b..2ad5b372 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ event.json private.md .claude/ tests/codes_compiled/ +private diff --git a/Makefile b/Makefile index 66ad9ec2..f672a442 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,10 @@ TINYGO_TARGETS := \ wasmx-fsm:28.finite_state_machine.wasm \ wasmx-raft-lib:2a.raft_library.wasm \ wasmx-raftp2p-lib:36.raftp2p_library.wasm \ - wasmx-ondemand-single-lib:65.wasmx_ondemand_single_library.wasm + wasmx-ondemand-single-lib:65.wasmx_ondemand_single_library.wasm \ + wasmx-kayrosp2p-lib:71.kayrosp2p_library.wasm \ + wasmx-kayrosp2p-ondemand-lib:72.kayrosp2p_ondemand_library.wasm \ + wasmx-kayros-verifier:75.kayros_verifier_0.0.1.wasm # wasmx-gov:35.gov_0.0.1.wasm \ # wasmx-gov-continuous:37.gov_cont_0.0.1.wasm \ @@ -72,7 +75,7 @@ $(TINYGO_MODULES): if [ -z "$$out" ]; then echo "Unknown TinyGo module: $$mod"; exit 1; fi; \ if [ ! -f "$(TINYGO_DIR)/$$mod/cmd/main.go" ]; then echo "No cmd/main.go in $$mod"; exit 1; fi; \ echo "Tidying $$mod..."; \ - (cd "$(TINYGO_DIR)/$$mod" && env GOWORK=off go mod tidy) 2>/dev/null; \ + (cd "$(TINYGO_DIR)/$$mod" && env GOWORK=off go mod tidy); \ echo "Building $$mod -> $(PRECOMPILE_DIR)/$$out"; \ - (cd "$(TINYGO_DIR)/$$mod" && env GOWORK=off tinygo build -o "$(abspath $(PRECOMPILE_DIR))/$$out" -no-debug -scheduler=none -gc=leaking -target=wasi ./cmd) 2>/dev/null; \ + (cd "$(TINYGO_DIR)/$$mod" && env GOWORK=off tinygo build -o "$(abspath $(PRECOMPILE_DIR))/$$out" -no-debug -scheduler=none -gc=leaking -target=wasi ./cmd); \ echo "Built $(PRECOMPILE_DIR)/$$out" diff --git a/go.work b/go.work index 3b5ddba6..79ccfe75 100644 --- a/go.work +++ b/go.work @@ -27,6 +27,7 @@ use ( ./tests/testdata/tinygo/wasmx-gov ./tests/testdata/tinygo/wasmx-gov-continuous ./tests/testdata/tinygo/wasmx-multichain-registry + ./tests/testdata/tinygo/wasmx-kayros-verifier // tests ./tests/testdata/tinygo/emailchain ./tests/testdata/tinygo/add diff --git a/tests/README.md b/tests/README.md index 10af0c74..81b33461 100644 --- a/tests/README.md +++ b/tests/README.md @@ -14,6 +14,9 @@ go test --count=1 -timeout 300s -v -run KeeperTestSuite/TestEwasmFibonacci ./x/w go test --count=1 -timeout 3000s -v -run TestKeeperTestSuite/TestWasmxSimpleStorage ./tests/wasmx -benchmark=true -wasm-runtime=wazero +go test --count=1 -timeout 3000s -v -run TestKeeperTestSuite/TestWasmxSimpleStorage ./tests/wasmx -wasm-runtime=wazero -consensus-label=consensus_kayrosp2p_ondemand_0.0.1 -kayros_user_key=0x0000000000000000000000000000000000000000000000000000000000000001 -kayros_base_url=https://kayros_indexer_url + + ``` * for wasmedge ```bash diff --git a/tests/kayros/keeper_test.go b/tests/kayros/keeper_test.go new file mode 100644 index 00000000..2d6004bd --- /dev/null +++ b/tests/kayros/keeper_test.go @@ -0,0 +1,87 @@ +package keeper_test + +import ( + "flag" + "os" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/stretchr/testify/suite" + + //nolint + + wt "github.com/loredanacirstea/wasmx/testutil/wasmx" + + wazero "github.com/loredanacirstea/wasmx-wazero" + + ut "github.com/loredanacirstea/mythos-tests/utils" +) + +var ( + wasmRuntime string + runKnownFixme bool +) + +// TestMain is the main entry point for the tests. +func TestMain(m *testing.M) { + flag.StringVar(&wasmRuntime, "wasm-runtime", "default", "Set the wasm runtime (e.g. wasmedge, wazero)") + flag.BoolVar(&runKnownFixme, "run-fixme", false, "Run skipped fixme tests") + + // Parse the flags. Only flags after `--` in `go test` command line will be passed here. + flag.Parse() + + os.Exit(m.Run()) +} + +// KeeperTestSuite is a testing suite to test keeper functions +type KeeperTestSuite struct { + wt.KeeperTestSuite +} + +var s *KeeperTestSuite + +func (suite *KeeperTestSuite) SetupSuite() { + suite.MaxBlockGas = 100_000_000_000 + suite.SystemContractsModify = ut.SystemContractsModify(wasmRuntime) + mydir, err := os.Getwd() + if err != nil { + panic(err) + } + + switch wasmRuntime { + case "wasmedge": + // suite.WasmVmMeta = wasmedge.WasmEdgeVmMeta{} + // suite.CompiledCacheDir = ut.GetCompiledCacheDir(mydir, "wasmedge") + panic("wasmedge not activated") + default: + // default runtime + suite.WasmVmMeta = wazero.NewWazeroVmMeta() + suite.CompiledCacheDir = ut.GetCompiledCacheDir(mydir, "wazero") + } + + suite.SetupChains() +} + +// TestKeeperTestSuite runs all the tests within this package. +func TestKeeperTestSuite(t *testing.T) { + s = new(KeeperTestSuite) + suite.Run(t, s) + + // Run Ginkgo integration tests + RegisterFailHandler(Fail) + RunSpecs(t, "Keeper Suite") +} + +func SkipFixmeTests(t *testing.T, name string) { + if !runKnownFixme { + t.Skipf("TODO: fixme %s", name) + } +} + +type VerifyProofRequest struct { + Data string `json:"data"` // base64 + DataType string `json:"data_type"` // hex + HashAlgo string `json:"hash_algo"` +} diff --git a/tests/kayros/verifier_test.go b/tests/kayros/verifier_test.go new file mode 100644 index 00000000..209b9478 --- /dev/null +++ b/tests/kayros/verifier_test.go @@ -0,0 +1,165 @@ +package keeper_test + +import ( + "encoding/base64" + "encoding/hex" + "encoding/json" + + "github.com/loredanacirstea/wasmx/x/wasmx/types" +) + +func (suite *KeeperTestSuite) TestKayrosVerifier() { + appA := s.AppContext() + verifierAddr, err := appA.App.WasmxKeeper.GetAddressOrRole(appA.Context(), types.ROLE_VERIFIER) + suite.Require().NoError(err) + + sender := suite.GetRandomAccount() + + apiBaseUrl := "" + apiUserKey := "" + if suite.StartNodeEnv != nil { + apiBaseUrl = suite.StartNodeEnv["kayros_base_url"] + apiUserKey = suite.StartNodeEnv["kayros_user_key"] + } + + if apiBaseUrl == "" || apiUserKey == "" { + suite.T().Skip("SKIPPING ... set kayros_base_url and kayros_user_key for TestKayrosVerifier") + } + + for _, pair := range KAYROS_TEST_BY_DATA { + dataTypeHex := pair[0] + dataHashHex := pair[1] + dataType, err := hex.DecodeString(dataTypeHex) + suite.Require().NoError(err) + dataHash, err := hex.DecodeString(dataHashHex) + suite.Require().NoError(err) + + verifyProofHashReq := map[string]any{ + "verify_proof_hash": map[string]any{ + "data_type": dataType, + "data_hash": dataHash, + "api_base_url": apiBaseUrl, + "api_user_key": apiUserKey, + }, + } + msg, err := json.Marshal(verifyProofHashReq) + suite.Require().NoError(err) + res := appA.WasmxQueryRaw(sender, verifierAddr, types.WasmxExecutionMessage{Data: msg}, nil, nil) + + var verifyResp struct { + Ok bool `json:"ok"` + Error string `json:"error"` + } + suite.Require().NoError(json.Unmarshal(res, &verifyResp)) + suite.Require().True(verifyResp.Ok, verifyResp.Error) + + if KAYROS_TOPLEVEL_HASH != "" { + verifyProofHashReq = map[string]any{ + "verify_proof_hash_with_inclusion": map[string]any{ + "data_type": dataType, + "data_hash": dataHash, + "hash_algo": "sha256", + "trusted_root_hash": KAYROS_TOPLEVEL_HASH, + "trusted_level": KAYROS_TOPLEVEL_LEVEL, + "trusted_position": KAYROS_TOPLEVEL_POSITION, + "api_base_url": apiBaseUrl, + "api_user_key": apiUserKey, + }, + } + msg, err = json.Marshal(verifyProofHashReq) + suite.Require().NoError(err) + res = appA.WasmxQueryRaw(sender, verifierAddr, types.WasmxExecutionMessage{Data: msg}, nil, nil) + + verifyResp = struct { + Ok bool `json:"ok"` + Error string `json:"error"` + }{} + suite.Require().NoError(json.Unmarshal(res, &verifyResp)) + suite.Require().True(verifyResp.Ok, verifyResp.Error) + } + } + + for _, proof := range KAYROS_TEST_PROOFS { + dataType, err := hex.DecodeString(proof.DataType) + suite.Require().NoError(err) + data, err := base64.StdEncoding.DecodeString(proof.Data) + suite.Require().NoError(err) + + verifyProofReq := map[string]any{ + "verify_proof": map[string]any{ + "data": data, + "data_type": dataType, + "hash_algo": proof.HashAlgo, + "api_base_url": apiBaseUrl, + "api_user_key": apiUserKey, + }, + } + msg, err := json.Marshal(verifyProofReq) + suite.Require().NoError(err) + res := appA.WasmxQueryRaw(sender, verifierAddr, types.WasmxExecutionMessage{Data: msg}, nil, nil) + + var verifyResp struct { + Ok bool `json:"ok"` + Error string `json:"error"` + } + suite.Require().NoError(json.Unmarshal(res, &verifyResp)) + suite.Require().True(verifyResp.Ok, verifyResp.Error) + + if KAYROS_TOPLEVEL_HASH != "" { + verifyProofReq = map[string]any{ + "verify_proof_with_inclusion": map[string]any{ + "data": data, + "data_type": dataType, + "hash_algo": proof.HashAlgo, + "trusted_root_hash": KAYROS_TOPLEVEL_HASH, + "trusted_level": KAYROS_TOPLEVEL_LEVEL, + "trusted_position": KAYROS_TOPLEVEL_POSITION, + "api_base_url": apiBaseUrl, + "api_user_key": apiUserKey, + }, + } + msg, err = json.Marshal(verifyProofReq) + suite.Require().NoError(err) + res = appA.WasmxQueryRaw(sender, verifierAddr, types.WasmxExecutionMessage{Data: msg}, nil, nil) + + verifyResp = struct { + Ok bool `json:"ok"` + Error string `json:"error"` + }{} + suite.Require().NoError(json.Unmarshal(res, &verifyResp)) + suite.Require().True(verifyResp.Ok, verifyResp.Error) + } + } +} + +var KAYROS_TEST_BY_HASH = []string{ + "c883af9b3ffc8261f6385e33dcd0bc825de1480dad4b5e9c9425c45c66655065", + "b605d60a6a531c57eba104a0e28a8be111e95d7a88a257066274de688eec1216", +} + +var KAYROS_TEST_BY_DATA = [][]string{ + { + "7761736d785f6d7974686f735f373030312d315f313736373039353338333930", + "374f1b87a48721c2a3f4d0777a6848e23edb13d0209a69f65330ee7ae84d5e9c", + }, +} + +// var KAYROS_TOPLEVEL_HASH = "be475ef98de5dcba176730b044a32239dbe5e8bb4f2172d5d8c555907eb445e0" +// var KAYROS_TOPLEVEL_LEVEL = 1 +// var KAYROS_TOPLEVEL_POSITION = 10 + +var KAYROS_TOPLEVEL_HASH = "" +var KAYROS_TOPLEVEL_LEVEL = -1 +var KAYROS_TOPLEVEL_POSITION = -1 + +var KAYROS_TEST_PROOFS = []VerifyProofRequest{ + { + Data: "CsoBCscBCiMvbXl0aG9zLndhc214LnYxLk1zZ0V4ZWN1dGVDb250cmFjdBKfAQotbXl0aG9zMTdzY2NsMDk1MzJod2g5eTRzeTlxNXNsd216ajdrZnBhNDJ5czY2Ei1teXRob3MxN2syMGVxanJ0czNlaDZteGtxcXNraHp2NGtqYWUwNTNrYzhkbGQaP3siZGF0YSI6ImV5SnpaWFFpT25zaWEyVjVJam9pYUdWc2JHOGlMQ0oyWVd4MVpTSTZJbk5oYlcxNUluMTkifRJvClAKRgofL2Nvc21vcy5jcnlwdG8uc2VjcDI1NmsxLlB1YktleRIjCiECqkwoU7+JW3gfOgr6as2ZJZD6Vo2Ob7H8sgOIIUOIrasSBAoCCAEYAxIbChMKBGFteXQSCzEwMDAwMDAwMDAwEICU69wDGkAMfcSxXine4wbRzIm1edq7DzGF089DRVHmeVI159CfqlcIS8adRZVD5ndJ4fFpUfkOjsg5DhLmQvyP+QGTynI0", + DataType: "7761736d785f74785f6d7974686f735f373030312d315f313736383834393037", + HashAlgo: "sha256", + // txhash + // 9bfbd26d8e9cc4f69eca1ffadc2e5db2891e0781efad9ec969e0cf980208eb10 + // kayros hash + // 5dbff8d307b6576610edea33ef2df6e53c93d9849b76f98b2665f59305d8dd14 + }, +} diff --git a/tests/testdata/tinygo/emailchain/main.go b/tests/testdata/tinygo/emailchain/main.go index e8a2452b..8b2d83a5 100644 --- a/tests/testdata/tinygo/emailchain/main.go +++ b/tests/testdata/tinygo/emailchain/main.go @@ -14,6 +14,10 @@ import ( //export wasmx_nondeterministic_1 func Wasmx_nondeterministic_1() {} +//go:wasm-module httpclient +//export wasmx_httpclient_i64_1 +func Wasmx_httpclient_i64_1() {} + //go:wasm-module wasmx //export memory_ptrlen_i64_1 func Wemory_ptrlen_i64_1() {} diff --git a/tests/testdata/tinygo/wasmx-env-httpclient/httpclient.go b/tests/testdata/tinygo/wasmx-env-httpclient/httpclient.go index 9df2b362..f55e0dd8 100644 --- a/tests/testdata/tinygo/wasmx-env-httpclient/httpclient.go +++ b/tests/testdata/tinygo/wasmx-env-httpclient/httpclient.go @@ -9,10 +9,6 @@ import ( utils "github.com/loredanacirstea/wasmx-env-utils" ) -//go:wasm-module httpclient -//export wasmx_httpclient_i64_1 -func wasmx_httpclient_i64_1() {} - //go:wasmimport httpclient Request func Request_(reqPtr int64) int64 diff --git a/tests/testdata/tinygo/wasmx-env/lib/wasmx_wrap.go b/tests/testdata/tinygo/wasmx-env/lib/wasmx_wrap.go index 80792452..8b3f099a 100644 --- a/tests/testdata/tinygo/wasmx-env/lib/wasmx_wrap.go +++ b/tests/testdata/tinygo/wasmx-env/lib/wasmx_wrap.go @@ -15,7 +15,6 @@ import ( ) func StorageStore(key, value []byte) { - Log([]byte("storagestore"), [][32]byte{}) StorageStore_(utils.BytesToPackedPtr(key), utils.BytesToPackedPtr(value)) } @@ -331,6 +330,11 @@ func Sha256(data []byte) []byte { return out } +func Keccak256(data []byte) []byte { + out := utils.PackedPtrToBytes(Keccak256_(utils.BytesToPackedPtr(data))) + return out +} + func MerkleHash(slices []string) []byte { payload := struct { Slices []string `json:"slices"` diff --git a/tests/testdata/tinygo/wasmx-fsm/cmd/main.go b/tests/testdata/tinygo/wasmx-fsm/cmd/main.go index 67fbf9e8..c718dffd 100644 --- a/tests/testdata/tinygo/wasmx-fsm/cmd/main.go +++ b/tests/testdata/tinygo/wasmx-fsm/cmd/main.go @@ -1,7 +1,9 @@ package main import ( + "encoding/base64" "encoding/json" + "fmt" "os" wasmx "github.com/loredanacirstea/wasmx-env/lib" @@ -90,7 +92,7 @@ func main() { result = []byte{} case calldata.StartNode != nil: - startNodeInternal(config) + startNodeInternal(config, calldata.StartNode.Data) result = []byte{} case calldata.SetupNode != nil: @@ -139,7 +141,7 @@ func eventual() { } func StartNode() { - configBz, _, err := lib.GetInterpreterCalldata() + configBz, calldBz, err := lib.GetInterpreterCalldata() if err != nil { lib.Revert("failed to get interpreter calldata: " + err.Error()) return @@ -151,7 +153,8 @@ func StartNode() { return } - startNodeInternal(config) + envData := string(calldBz) + startNodeInternal(config, envData) // Handle finish data result := wasmx.GetFinishData() @@ -236,8 +239,28 @@ func setupNodeInternal(config lib.MachineExternal, data string) { lib.RunInternal(config, event) } -func startNodeInternal(config lib.MachineExternal) { +func startNodeInternal(config lib.MachineExternal, envData string) { lib.LoggerInfo("emit start event", []string{"module", lib.MODULE_NAME}) + + // Parse and store env variables in context + if envData != "" { + envBz, err := base64.StdEncoding.DecodeString(envData) + if err == nil { + var env map[string]string + if err := json.Unmarshal(envBz, &env); err == nil { + lib.LoggerInfo("storing env variables in context", []string{"count", fmt.Sprintf("%d", len(env))}) + for key, value := range env { + lib.SetContextValue(key, value) + lib.LoggerDebug("set env variable in context", []string{"key", key}) + } + } else { + lib.LoggerError("failed to unmarshal env data", []string{"error", err.Error()}) + } + } else { + lib.LoggerError("failed to decode env data", []string{"error", err.Error()}) + } + } + event := lib.EventObject{ Type: "start", Params: []lib.ActionParam{}, diff --git a/tests/testdata/tinygo/wasmx-fsm/lib/timer.go b/tests/testdata/tinygo/wasmx-fsm/lib/timer.go index c3b3e0fa..e4457a54 100644 --- a/tests/testdata/tinygo/wasmx-fsm/lib/timer.go +++ b/tests/testdata/tinygo/wasmx-fsm/lib/timer.go @@ -12,7 +12,7 @@ func GetLastIntervalId() int64 { if value == "" { return 0 } - result, err := parseInt64(value) + result, err := ParseInt64(value) if err != nil { return 0 } @@ -46,7 +46,7 @@ func GetLastIntervalIdForState(state, delay string) int64 { if lastIntervalId == "" { return 0 } - result, err := parseInt64(lastIntervalId) + result, err := ParseInt64(lastIntervalId) if err != nil { return 0 } diff --git a/tests/testdata/tinygo/wasmx-fsm/lib/utils.go b/tests/testdata/tinygo/wasmx-fsm/lib/utils.go index 51f7b780..835cfaa1 100644 --- a/tests/testdata/tinygo/wasmx-fsm/lib/utils.go +++ b/tests/testdata/tinygo/wasmx-fsm/lib/utils.go @@ -60,7 +60,7 @@ func getAddressHex(addr []byte) string { } // Parsing utilities -func parseInt32(s string) (int32, error) { +func ParseInt32(s string) (int32, error) { val, err := strconv.ParseInt(s, 10, 32) if err != nil { return 0, err @@ -68,7 +68,7 @@ func parseInt32(s string) (int32, error) { return int32(val), nil } -func parseInt64(s string) (int64, error) { +func ParseInt64(s string) (int64, error) { return strconv.ParseInt(s, 10, 64) } diff --git a/tests/testdata/tinygo/wasmx-kayros-verifier/README.md b/tests/testdata/tinygo/wasmx-kayros-verifier/README.md new file mode 100644 index 00000000..601e5930 --- /dev/null +++ b/tests/testdata/tinygo/wasmx-kayros-verifier/README.md @@ -0,0 +1,15 @@ +# wasmx-kayros-verifier + +TinyGo library that verifies Kayros records for wasmx consensus. + +What it does +- Validates record hash: `hash(prev_hash || data_type || data_item) == hash_item`. +- Validates chain linkage: `prev_hash` equals previous record `hash_item`. +- Validates timestamp and UUID consistency. +- Validates level rollups using Kayros level hashes. + +Kayros levels +- Level 1 hashes roll up base records in groups of 256. +- Level 2 hashes roll up Level 1 hashes, and so on. +- To verify inclusion, supply the hashes used at each level (per position) and compare + the computed rollup to the Kayros level hash. diff --git a/tests/testdata/tinygo/wasmx-kayros-verifier/cmd/main.go b/tests/testdata/tinygo/wasmx-kayros-verifier/cmd/main.go new file mode 100644 index 00000000..2754dc3b --- /dev/null +++ b/tests/testdata/tinygo/wasmx-kayros-verifier/cmd/main.go @@ -0,0 +1,155 @@ +package main + +import ( + "encoding/hex" + "encoding/json" + + wasmx "github.com/loredanacirstea/wasmx-env/lib" + verifier "github.com/loredanacirstea/wasmx-kayros-verifier/lib" +) + +//go:wasm-module wasmx +//export memory_ptrlen_i64_1 +func Memory_ptrlen_i64_1() {} + +//go:wasm-module wasmx +//export wasmx_env_i64_2 +func Wasmx_env_i64_2() {} + +//go:wasm-module httpclient +//export wasmx_httpclient_i64_1 +func Wasmx_httpclient_i64_1() {} + +func respond(ok bool, errMsg string) { + resp := verifier.VerifyResponse{Ok: ok, Error: errMsg} + bz, err := json.Marshal(resp) + if err != nil { + wasmx.Revert([]byte("failed to marshal response: " + err.Error())) + } + wasmx.Finish(bz) +} + +func configFrom(baseUrl string, userKey string) verifier.KayrosConfig { + return verifier.KayrosConfig{ + ApiBaseUrl: baseUrl, + ApiUserKey: userKey, + } +} + +//go:wasm-module wasmx-kayros-verifier +//export instantiate +func Instantiate() { + databz := wasmx.GetCallData() + if len(databz) == 0 { + wasmx.Finish([]byte{}) + return + } + var payload map[string]any + if err := json.Unmarshal(databz, &payload); err != nil { + wasmx.Revert([]byte("invalid call data: " + err.Error() + ": " + string(databz))) + } + wasmx.Finish([]byte{}) +} + +func main() { + databz := wasmx.GetCallData() + calld := &verifier.Calldata{} + if err := json.Unmarshal(databz, calld); err != nil { + wasmx.Revert([]byte("invalid call data: " + err.Error() + ": " + string(databz))) + } + + switch { + case calld.VerifyProof != nil: + req := *calld.VerifyProof + if len(req.Data) == 0 { + respond(false, "missing data") + return + } + dataTypeHex := wasmx.HexString(hex.EncodeToString(req.DataType)) + ok, errMsg := verifier.VerifyProof(req.Data, dataTypeHex, req.HashAlgo, configFrom(req.ApiBaseUrl, req.ApiUserKey)) + respond(ok, errMsg) + return + case calld.VerifyProofWithInclusion != nil: + req := *calld.VerifyProofWithInclusion + if len(req.Data) == 0 { + respond(false, "missing data") + return + } + dataTypeHex := wasmx.HexString(hex.EncodeToString(req.DataType)) + ok, errMsg := verifier.VerifyProofWithInclusion( + req.Data, + dataTypeHex, + req.HashAlgo, + req.TrustedRootHash, + req.TrustedLevel, + req.TrustedPosition, + configFrom(req.ApiBaseUrl, req.ApiUserKey), + ) + respond(ok, errMsg) + return + case calld.VerifyProofHash != nil: + req := *calld.VerifyProofHash + dataTypeHex := wasmx.HexString(hex.EncodeToString(req.DataType)) + dataHashHex := wasmx.HexString(hex.EncodeToString(req.DataHash)) + ok, errMsg := verifier.VerifyProofHash(dataTypeHex, dataHashHex, configFrom(req.ApiBaseUrl, req.ApiUserKey)) + respond(ok, errMsg) + return + case calld.VerifyProofHashWithInclusion != nil: + req := *calld.VerifyProofHashWithInclusion + dataTypeHex := wasmx.HexString(hex.EncodeToString(req.DataType)) + dataHashHex := wasmx.HexString(hex.EncodeToString(req.DataHash)) + ok, errMsg := verifier.VerifyProofHashWithInclusion( + dataTypeHex, + dataHashHex, + req.HashAlgo, + req.TrustedRootHash, + req.TrustedLevel, + req.TrustedPosition, + configFrom(req.ApiBaseUrl, req.ApiUserKey), + ) + respond(ok, errMsg) + return + case calld.VerifyRecordHash != nil: + req := *calld.VerifyRecordHash + ok, errMsg := verifier.VerifyRecordHash(&req.Record, req.HashAlgo) + respond(ok, errMsg) + return + case calld.VerifyRecordChainLink != nil: + req := *calld.VerifyRecordChainLink + ok, errMsg := verifier.VerifyRecordChainLink(&req.Record, &req.Prev) + respond(ok, errMsg) + return + case calld.VerifyRecordTimestamp != nil: + req := *calld.VerifyRecordTimestamp + ok, errMsg := verifier.VerifyRecordTimestamp(&req.Record) + respond(ok, errMsg) + return + case calld.VerifyRecordUUID != nil: + req := *calld.VerifyRecordUUID + ok, errMsg := verifier.VerifyRecordUUID(&req.Record) + respond(ok, errMsg) + return + case calld.VerifyLevelProof != nil: + req := *calld.VerifyLevelProof + ok, errMsg := verifier.VerifyLevelProof(configFrom(req.ApiBaseUrl, req.ApiUserKey), req.Proof, req.HashAlgo) + respond(ok, errMsg) + return + case calld.VerifyProofPath != nil: + req := *calld.VerifyProofPath + ok, errMsg := verifier.VerifyProofPath(&req.Proof, req.HashAlgo) + respond(ok, errMsg) + return + case calld.VerifyKayrosRecord != nil: + req := *calld.VerifyKayrosRecord + ok, errMsg := verifier.VerifyKayrosRecord(&req.Record, &req.Prev, req.HashAlgo, req.LevelProofs, configFrom(req.ApiBaseUrl, req.ApiUserKey)) + respond(ok, errMsg) + return + case calld.VerifyKayrosRecordWithProof != nil: + req := *calld.VerifyKayrosRecordWithProof + ok, errMsg := verifier.VerifyKayrosRecordWithProof(&req.Record, &req.Prev, &req.Proof, req.HashAlgo) + respond(ok, errMsg) + return + } + + wasmx.Revert(append([]byte("invalid function call data: "), databz...)) +} diff --git a/tests/testdata/tinygo/wasmx-kayros-verifier/go.mod b/tests/testdata/tinygo/wasmx-kayros-verifier/go.mod new file mode 100644 index 00000000..60876ca5 --- /dev/null +++ b/tests/testdata/tinygo/wasmx-kayros-verifier/go.mod @@ -0,0 +1,23 @@ +module github.com/loredanacirstea/wasmx-kayros-verifier + +go 1.24 + +toolchain go1.24.4 + +require github.com/loredanacirstea/wasmx-env v0.0.0 + +require github.com/loredanacirstea/wasmx-env-httpclient v0.0.0 + +require github.com/loredanacirstea/wasmx-env-utils v0.0.0 // indirect + +require cosmossdk.io/math v1.5.3 // indirect + +replace github.com/loredanacirstea/wasmx-env v0.0.0 => ../wasmx-env + +replace github.com/loredanacirstea/wasmx-env-httpclient v0.0.0 => ../wasmx-env-httpclient + +replace github.com/loredanacirstea/wasmx-fsm v0.0.0 => ../wasmx-fsm + +replace github.com/loredanacirstea/wasmx-env-utils v0.0.0 => ../wasmx-env-utils + +replace github.com/loredanacirstea/wasmx-utils v0.0.0 => ../wasmx-utils diff --git a/tests/testdata/tinygo/wasmx-kayros-verifier/go.sum b/tests/testdata/tinygo/wasmx-kayros-verifier/go.sum new file mode 100644 index 00000000..18cf05fc --- /dev/null +++ b/tests/testdata/tinygo/wasmx-kayros-verifier/go.sum @@ -0,0 +1,12 @@ +cosmossdk.io/math v1.5.3 h1:WH6tu6Z3AUCeHbeOSHg2mt9rnoiUWVWaQ2t6Gkll96U= +cosmossdk.io/math v1.5.3/go.mod h1:uqcZv7vexnhMFJF+6zh9EWdm/+Ylyln34IvPnBauPCQ= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/tests/testdata/tinygo/wasmx-kayros-verifier/lib/kayros.go b/tests/testdata/tinygo/wasmx-kayros-verifier/lib/kayros.go new file mode 100644 index 00000000..eb7fda7e --- /dev/null +++ b/tests/testdata/tinygo/wasmx-kayros-verifier/lib/kayros.go @@ -0,0 +1,349 @@ +package lib + +import ( + "encoding/json" + "fmt" + "net/http" + + httpclient "github.com/loredanacirstea/wasmx-env-httpclient" + wasmx "github.com/loredanacirstea/wasmx-env/lib" +) + +// KayrosClient provides methods to interact with Kayros API +type KayrosClient struct { + config KayrosConfig +} + +// NewKayrosClient creates a new Kayros API client with the given configuration +func NewKayrosClient(config KayrosConfig) *KayrosClient { + return &KayrosClient{ + config: config, + } +} + +// makeRequest performs an HTTP GET request to the Kayros API +func (kc *KayrosClient) makeRequest(endpoint string) ([]byte, error) { + url := fmt.Sprintf("%s%s", kc.config.ApiBaseUrl, endpoint) + + headers := http.Header{ + "Content-Type": []string{"application/json"}, + } + if kc.config.ApiUserKey != "" { + headers["X-User-Key"] = []string{kc.config.ApiUserKey} + } + + req := httpclient.HttpRequestWrap{ + Request: httpclient.HttpRequest{ + Method: "GET", + Url: url, + Header: headers, + Data: []byte{}, + }, + ResponseHandler: httpclient.ResponseHandler{ + MaxSize: 1024 * 1024, + FilePath: "", + }, + } + LoggerDebug("http request", []string{"url", url}) + resp := httpclient.Request(&req) + + if resp.Error != "" { + return nil, fmt.Errorf("HTTP request failed: %s", resp.Error) + } + if resp.Data.StatusCode != 200 { + return nil, fmt.Errorf("HTTP request returned status %d: %s", resp.Data.StatusCode, resp.Data.Status) + } + return resp.Data.Data, nil +} + +// GetRecordByDataItem retrieves a Kayros record by data_type and data_item (transaction hash) +func (kc *KayrosClient) GetRecordByDataItem(dataType wasmx.HexString, txHash wasmx.HexString) (*KayrosRecord, error) { + return kc.GetRecord(dataType, txHash) +} + +// GetRecordByHash retrieves a Kayros record by hash_item +func (kc *KayrosClient) GetRecordByHash(hashItem string) (*KayrosRecord, error) { + endpoint := fmt.Sprintf("/api/database/record-by-hash?hash_item=%s", hashItem) + + respData, err := kc.makeRequest(endpoint) + if err != nil { + return nil, err + } + + var kayrosResp KayrosRecordResponse + if err := json.Unmarshal(respData, &kayrosResp); err != nil { + return nil, fmt.Errorf("failed to unmarshal Kayros response: %w", err) + } + if !kayrosResp.Success { + return nil, fmt.Errorf("Kayros API error: %s", kayrosResp.Message) + } + return &kayrosResp.Data, nil +} + +// GetRecordWithPrev retrieves a Kayros record with its previous hash by UUID +func (kc *KayrosClient) GetRecordWithPrev(dataType wasmx.HexString, uuid string) (*KayrosRecord, error) { + endpoint := fmt.Sprintf("/api/database/record-with-prev?data_type=%s&uuid=%s", + dataType, uuid) + + respData, err := kc.makeRequest(endpoint) + if err != nil { + return nil, err + } + + var kayrosResp KayrosRecordResponse + if err := json.Unmarshal(respData, &kayrosResp); err != nil { + return nil, fmt.Errorf("failed to unmarshal Kayros response: %w", err) + } + if !kayrosResp.Success { + return nil, fmt.Errorf("Kayros API error: %s", kayrosResp.Message) + } + return &kayrosResp.Data, nil +} + +// GetRecordsFromPrev retrieves multiple Kayros records from a previous UUID with a limit +func (kc *KayrosClient) GetRecordsFromPrev(dataType wasmx.HexString, uuid string, limit int) ([]KayrosRecord, error) { + endpoint := fmt.Sprintf("/api/database/records-since-prev?data_type=%s&uuid=%s&limit=%d", + dataType, uuid, limit) + + respData, err := kc.makeRequest(endpoint) + if err != nil { + return nil, err + } + + var kayrosResp KayrosRecordsResponse + if err := json.Unmarshal(respData, &kayrosResp); err != nil { + return nil, fmt.Errorf("failed to unmarshal Kayros response: %w", err) + } + if !kayrosResp.Success { + return nil, fmt.Errorf("Kayros API error: %s", kayrosResp.Message) + } + return kayrosResp.Data.Records, nil +} + +// makePostRequest performs an HTTP POST request to the Kayros API +func (kc *KayrosClient) makePostRequest(endpoint string, body []byte) ([]byte, error) { + url := fmt.Sprintf("%s%s", kc.config.ApiBaseUrl, endpoint) + + userKey := kc.config.ApiUserKey + + req := httpclient.HttpRequestWrap{ + Request: httpclient.HttpRequest{ + Method: "POST", + Url: url, + Header: http.Header{ + "Content-Type": []string{"application/json"}, + "X-User-Key": []string{userKey}, + }, + Data: body, + }, + ResponseHandler: httpclient.ResponseHandler{ + MaxSize: 1024 * 1024, + FilePath: "", + }, + } + reqbz, _ := json.Marshal(&req) + LoggerDebug("http request", []string{"url", url, "method", "POST", "data", string(body), "req", string(reqbz)}) + resp := httpclient.Request(&req) + + if resp.Error != "" { + return nil, fmt.Errorf("HTTP POST request failed: %s", resp.Error) + } + if resp.Data.StatusCode != 200 && resp.Data.StatusCode != 201 { + return nil, fmt.Errorf("HTTP POST request returned status %d: %s", resp.Data.StatusCode, resp.Data.Status) + } + LoggerDebug("http request", []string{"url", url, "method", "POST", "response", string(resp.Data.Data)}) + + return resp.Data.Data, nil +} + +// GetBlockTimestamp retrieves or registers a block hash with Kayros to get a deterministic timestamp +func (kc *KayrosClient) GetBlockTimestamp(blockDataType wasmx.HexString, blockHashHex wasmx.HexString) (string, error) { + record, err := kc.GetBlockRecord(blockDataType, blockHashHex) + if err == nil && record != nil { + timestamp, err := TimeuuidHexToTimestamp(record.UuidHex) + if err != nil { + return "", fmt.Errorf("failed to extract timestamp from existing record UUID: %w: %s", err, record.UuidHex) + } + return timestamp, nil + } + + LoggerDebug("registering block hash with Kayros", []string{"blockHash", string(blockHashHex)}) + + _, err = kc.RegisterBlockHash(blockDataType, blockHashHex) + if err != nil { + return "", fmt.Errorf("failed to register block hash: %w", err) + } + + record, err = kc.GetBlockRecord(blockDataType, blockHashHex) + if err != nil { + return "", fmt.Errorf("failed to retrieve block record after registration: %w", err) + } + + timestamp, err := TimeuuidHexToTimestamp(record.UuidHex) + if err != nil { + return "", fmt.Errorf("failed to extract timestamp from record UUID: %w", err) + } + + LoggerInfo("block hash registered and queried from Kayros", []string{ + "uuid", record.UuidHex, + "timestamp", timestamp, + }) + return timestamp, nil +} + +// GetBlockRecord retrieves a block record from Kayros by block hash +func (kc *KayrosClient) GetBlockRecord(blockDataType wasmx.HexString, blockHashHex wasmx.HexString) (*KayrosRecord, error) { + return kc.GetRecord(blockDataType, blockHashHex) +} + +// GetRecord retrieves a record from Kayros by data_type and data_item +func (kc *KayrosClient) GetRecord(dataType wasmx.HexString, dataItem wasmx.HexString) (*KayrosRecord, error) { + endpoint := fmt.Sprintf("/api/database/record?data_type=%s&data_item=%s", dataType, dataItem) + + respData, err := kc.makeRequest(endpoint) + if err != nil { + return nil, err + } + + var kayrosResp KayrosRecordsResponse + if err := json.Unmarshal(respData, &kayrosResp); err != nil { + return nil, fmt.Errorf("failed to unmarshal Kayros response: %w", err) + } + if !kayrosResp.Success { + return nil, fmt.Errorf("Kayros API error: %s", kayrosResp.Message) + } + if len(kayrosResp.Data.Records) < 1 { + return nil, fmt.Errorf("Kayros API record not found: %s", kayrosResp.Message) + } + return &kayrosResp.Data.Records[0], nil +} + +// GetLevelHash retrieves a rollup hash by level and position. +func (kc *KayrosClient) GetLevelHash(level int, position int) (*LevelHashEntry, error) { + endpoint := fmt.Sprintf("/api/database/level-hash?level=%d&position=%d", level, position) + + respData, err := kc.makeRequest(endpoint) + if err != nil { + return nil, err + } + + var kayrosResp LevelHashResponse + if err := json.Unmarshal(respData, &kayrosResp); err != nil { + return nil, fmt.Errorf("failed to unmarshal Kayros response: %w", err) + } + if !kayrosResp.Success { + return nil, fmt.Errorf("Kayros API error: %s", kayrosResp.Message) + } + + return &LevelHashEntry{ + Position: kayrosResp.Data.Position, + HashHex: kayrosResp.Data.HashHex, + UuidHex: "", + }, nil +} + +// GetLevelRange retrieves a set of rollup hashes for a level. +func (kc *KayrosClient) GetLevelRange(level int, limit int) ([]LevelHashEntry, error) { + endpoint := fmt.Sprintf("/api/database/level-range?level=%d&limit=%d", level, limit) + + respData, err := kc.makeRequest(endpoint) + if err != nil { + return nil, err + } + + var kayrosResp LevelRangeResponse + if err := json.Unmarshal(respData, &kayrosResp); err != nil { + return nil, fmt.Errorf("failed to unmarshal Kayros response: %w", err) + } + if !kayrosResp.Success { + return nil, fmt.Errorf("Kayros API error: %s", kayrosResp.Message) + } + + return kayrosResp.Data.Entries, nil +} + +// GetProofPath retrieves the proof path for a hash_item. +func (kc *KayrosClient) GetProofPath(hashItem string) (*ProofPathData, error) { + endpoint := fmt.Sprintf("/api/proof?hash_item=%s", hashItem) + + respData, err := kc.makeRequest(endpoint) + if err != nil { + return nil, err + } + + var kayrosResp ProofPathResponse + if err := json.Unmarshal(respData, &kayrosResp); err != nil { + return nil, fmt.Errorf("failed to unmarshal Kayros response: %w", err) + } + if !kayrosResp.Success { + return nil, fmt.Errorf("Kayros API error: %s", kayrosResp.Message) + } + + return &kayrosResp.Data, nil +} + +// GetBlockRecordByHashItem retrieves a block record from Kayros by hash_item +func (kc *KayrosClient) GetBlockRecordByHashItem(hashItem string) (*KayrosRecord, error) { + endpoint := fmt.Sprintf("/api/database/record-by-hash?hash_item=%s", hashItem) + + respData, err := kc.makeRequest(endpoint) + if err != nil { + return nil, err + } + + var kayrosResp KayrosRecordResponse + if err := json.Unmarshal(respData, &kayrosResp); err != nil { + return nil, fmt.Errorf("failed to unmarshal Kayros response: %w", err) + } + if !kayrosResp.Success { + return nil, fmt.Errorf("Kayros API error: %s", kayrosResp.Message) + } + return &kayrosResp.Data, nil +} + +// RegisterBlockHash registers a block hash with Kayros for timestamp synchronization +func (kc *KayrosClient) RegisterBlockHash(blockDataType wasmx.HexString, blockHashHex wasmx.HexString) (*KayrosRegistrationResponse, error) { + reqBody := KayrosRegistrationRequest{ + DataType: blockDataType, + DataItem: blockHashHex, + } + return kc.RegisterData(reqBody) +} + +// RegisterTransaction registers a transaction hash with Kayros for ordering +func (kc *KayrosClient) RegisterTransaction(dataType wasmx.HexString, txHash wasmx.HexString) (*KayrosRegistrationResponse, error) { + reqBody := KayrosRegistrationRequest{ + DataType: dataType, + DataItem: txHash, + } + return kc.RegisterData(reqBody) +} + +// RegisterData registers a data item with Kayros +func (kc *KayrosClient) RegisterData(reqBody KayrosRegistrationRequest) (*KayrosRegistrationResponse, error) { + endpoint := "/api/grpc/single-hash" + reqbz, err := json.Marshal(&reqBody) + if err != nil { + return nil, fmt.Errorf("failed to marshal request: %w", err) + } + + respData, err := kc.makePostRequest(endpoint, reqbz) + if err != nil { + return nil, err + } + + var kayrosResp KayrosRegistrationResponseWrap + if err := json.Unmarshal(respData, &kayrosResp); err != nil { + return nil, fmt.Errorf("failed to unmarshal Kayros response: %w", err) + } + + LoggerDebugExtended(`Kayros registration`, []string{"endpoint", endpoint, "success", fmt.Sprintf("%v", kayrosResp.Success), "kayros_hash", string(kayrosResp.Data.ComputedHash), "timeuuid", string(kayrosResp.Data.TimeUUID)}) + + if !kayrosResp.Success { + return nil, fmt.Errorf("Failed Kayros registration request: %s", kayrosResp.Message) + } + if !kayrosResp.Data.Success { + return nil, fmt.Errorf("Failed Kayros registration: %s; %s", kayrosResp.Message, kayrosResp.Data.Message) + } + return &kayrosResp.Data, nil +} diff --git a/tests/testdata/tinygo/wasmx-kayros-verifier/lib/proof.go b/tests/testdata/tinygo/wasmx-kayros-verifier/lib/proof.go new file mode 100644 index 00000000..fcf81315 --- /dev/null +++ b/tests/testdata/tinygo/wasmx-kayros-verifier/lib/proof.go @@ -0,0 +1,388 @@ +package lib + +import ( + "encoding/hex" + "fmt" + "strings" + "time" + + wasmx "github.com/loredanacirstea/wasmx-env/lib" +) + +type LevelProof struct { + Level int + Position int + Hashes []string +} + +// VerifyProof hashes data and verifies it exists in Kayros. +func VerifyProof(data []byte, dataType wasmx.HexString, hashAlgo string, cfg KayrosConfig) (bool, string) { + hashAlgo = strings.ToLower(strings.TrimSpace(hashAlgo)) + if hashAlgo == "" || hashAlgo == "sha256" { + return VerifyProofHash(dataType, wasmx.HexString(hex.EncodeToString(wasmx.Sha256(data))), cfg) + } + if hashAlgo == "keccak256" { + return VerifyProofHash(dataType, wasmx.HexString(hex.EncodeToString(wasmx.Keccak256(data))), cfg) + } + return false, "unsupported hash algorithm" +} + +// VerifyProofHash checks if a Kayros record exists for the dataType and dataHash. +func VerifyProofHash(dataType wasmx.HexString, dataHash wasmx.HexString, cfg KayrosConfig) (bool, string) { + client := NewKayrosClient(cfg) + record, err := client.GetRecord(dataType, dataHash) + if err != nil || record == nil { + return false, "no record found" + } + + if record.DataTypeHex != string(dataType) || record.DataItemHex != string(dataHash) { + return false, "record mismatch" + } + return true, "" +} + +// VerifyProofWithInclusion hashes data and verifies the record + proof path inclusion. +func VerifyProofWithInclusion(data []byte, dataType wasmx.HexString, hashAlgo string, trustedRootHash string, trustedLevel int, trustedPosition int, cfg KayrosConfig) (bool, string) { + hashAlgo = strings.ToLower(strings.TrimSpace(hashAlgo)) + if hashAlgo == "" || hashAlgo == "sha256" { + return VerifyProofHashWithInclusion(dataType, wasmx.HexString(hex.EncodeToString(wasmx.Sha256(data))), hashAlgo, trustedRootHash, trustedLevel, trustedPosition, cfg) + } + if hashAlgo == "keccak256" { + return VerifyProofHashWithInclusion(dataType, wasmx.HexString(hex.EncodeToString(wasmx.Keccak256(data))), hashAlgo, trustedRootHash, trustedLevel, trustedPosition, cfg) + } + return false, "unsupported hash algorithm" +} + +// VerifyProofHashWithInclusion verifies record details and proof path against a trusted root hash. +func VerifyProofHashWithInclusion(dataType wasmx.HexString, dataHash wasmx.HexString, hashAlgo string, trustedRootHash string, trustedLevel int, trustedPosition int, cfg KayrosConfig) (bool, string) { + client := NewKayrosClient(cfg) + record, err := client.GetRecord(dataType, dataHash) + if err != nil || record == nil { + return false, "no record found" + } + if record.DataTypeHex != string(dataType) || record.DataItemHex != string(dataHash) { + return false, "record mismatch" + } + + var prev *KayrosRecord + if strings.TrimSpace(record.PrevHashHex) != "" { + prev, err = client.GetRecordByHash(record.PrevHashHex) + if err != nil || prev == nil { + return false, "previous record not found" + } + } + + if ok, errMsg := VerifyRecordHash(record, hashAlgo); !ok { + return false, errMsg + } + if prev != nil { + if ok, errMsg := VerifyRecordChainLink(record, prev); !ok { + return false, errMsg + } + } + if ok, errMsg := VerifyRecordUUID(record); !ok { + return false, errMsg + } + + proof, err := client.GetProofPath(record.HashItemHex) + if err != nil || proof == nil { + return false, "missing proof path" + } + if !strings.EqualFold(proof.HashItemHex, record.HashItemHex) { + return false, "hash_item mismatch" + } + if !strings.EqualFold(proof.DataTypeHex, record.DataTypeHex) { + return false, "data_type mismatch" + } + if !strings.EqualFold(proof.DataItemHex, record.DataItemHex) { + return false, "data_item mismatch" + } + if trustedRootHash == "" { + return false, "missing trusted root hash" + } + if !strings.EqualFold(proof.RootHashHex, trustedRootHash) { + return false, "root hash mismatch" + } + + if trustedLevel >= 0 && trustedPosition >= 0 { + entry, err := client.GetLevelHash(trustedLevel, trustedPosition) + if err != nil || entry == nil { + return false, "trusted level hash not found" + } + if !strings.EqualFold(entry.HashHex, trustedRootHash) { + return false, "trusted level hash mismatch" + } + } + + if ok, errMsg := VerifyProofPath(proof, hashAlgo); !ok { + return false, errMsg + } + return true, "" +} + +// VerifyRecordHash checks hash_item = hash(prev_hash || data_type || data_item). +func VerifyRecordHash(record *KayrosRecord, hashAlgo string) (bool, string) { + if record == nil { + return false, "record is nil" + } + if strings.TrimSpace(hashAlgo) == "" && strings.TrimSpace(record.HashType) != "" { + hashAlgo = record.HashType + } + prevBytes, err := decodeHexOrEmpty(record.PrevHashHex) + if err != nil { + return false, "invalid prev_hash" + } + dataTypeBytes, err := decodeHex(record.DataTypeHex) + if err != nil { + return false, "invalid data_type" + } + dataItemBytes, err := decodeHex(record.DataItemHex) + if err != nil { + return false, "invalid data_item" + } + uuidBytes, err := decodeHex(record.UuidHex) + if err != nil { + return false, "invalid uuid" + } + if len(uuidBytes) != 16 { + return false, "invalid uuid length" + } + + payload := append(append(append(prevBytes, dataTypeBytes...), dataItemBytes...), uuidBytes...) + computed, errMsg := hashBytes(payload, hashAlgo) + if errMsg != "" { + return false, errMsg + } + computedHex := hex.EncodeToString(computed) + if !strings.EqualFold(computedHex, record.HashItemHex) { + return false, fmt.Sprintf("hash mismatch computed=%s record=%s", computedHex, record.HashItemHex) + } + return true, "" +} + +// VerifyRecordChainLink checks record.prev_hash equals prev.hash_item and data_type matches. +func VerifyRecordChainLink(record *KayrosRecord, prev *KayrosRecord) (bool, string) { + if record == nil || prev == nil { + return false, "missing record chain" + } + if !strings.EqualFold(record.DataTypeHex, prev.DataTypeHex) { + return false, "data_type mismatch" + } + if !strings.EqualFold(record.PrevHashHex, prev.HashItemHex) { + return false, "prev_hash mismatch" + } + return true, "" +} + +// VerifyRecordTimestamp ensures timestamp is valid RFC3339Nano. +func VerifyRecordTimestamp(record *KayrosRecord) (bool, string) { + if record == nil { + return false, "record is nil" + } + if record.Timestamp == "" { + return false, "missing timestamp" + } + if _, err := parseRecordTimestamp(record.Timestamp); err != nil { + return false, "invalid timestamp: " + record.Timestamp + } + return true, "" +} + +// VerifyRecordUUID ensures UUID timestamp matches record timestamp. +func VerifyRecordUUID(record *KayrosRecord) (bool, string) { + if record == nil { + return false, "record is nil" + } + recordTime, err := parseRecordTimestamp(record.Timestamp) + if err != nil { + return false, "invalid timestamp: " + record.Timestamp + } + ts, err := TimeuuidHexToTimestamp(record.UuidHex) + if err != nil { + return false, "invalid uuid" + } + uuidTime, err := parseRecordTimestamp(ts) + if err != nil { + return false, "invalid uuid timestamp" + } + recordTime = recordTime.UTC().Truncate(time.Millisecond) + uuidTime = uuidTime.UTC().Truncate(time.Millisecond) + if !recordTime.Equal(uuidTime) { + return false, "uuid does not match timestamp" + } + return true, "" +} + +func parseRecordTimestamp(value string) (time.Time, error) { + if t, err := time.Parse(time.RFC3339Nano, value); err == nil { + return t, nil + } + if t, err := time.Parse(time.RFC3339, value); err == nil { + return t, nil + } + return time.Parse("2006-01-02 15:04:05.999 MST", value) +} + +// VerifyLevelProof checks a single level rollup hash against Kayros. +func VerifyLevelProof(cfg KayrosConfig, proof LevelProof, hashAlgo string) (bool, string) { + if proof.Level < 1 { + return false, "invalid level" + } + if proof.Position < 0 { + return false, "invalid position" + } + if len(proof.Hashes) == 0 { + return false, "missing level hashes" + } + + rollupBytes, errMsg := hashHexConcat(proof.Hashes, hashAlgo) + if errMsg != "" { + return false, errMsg + } + + client := NewKayrosClient(cfg) + expected, err := client.GetLevelHash(proof.Level, proof.Position) + if err != nil || expected == nil { + return false, "no level hash found" + } + if !strings.EqualFold(hex.EncodeToString(rollupBytes), expected.HashHex) { + return false, "level hash mismatch" + } + return true, "" +} + +// VerifyProofPath verifies the proof path against computed rollup hashes and root hash. +func VerifyProofPath(path *ProofPathData, hashAlgo string) (bool, string) { + if path == nil { + return false, "missing proof path" + } + if len(path.Proof) == 0 { + return false, "empty proof path" + } + + hashItem := strings.ToLower(strings.TrimSpace(path.HashItemHex)) + if hashItem == "" { + return false, "missing hash_item" + } + rootHash := strings.ToLower(strings.TrimSpace(path.RootHashHex)) + if rootHash == "" { + return false, "missing root hash" + } + + for _, step := range path.Proof { + if step.Position < 0 { + return false, "invalid position" + } + if step.IndexInBlock < 0 || step.IndexInBlock >= 256 { + return false, "invalid index_in_block" + } + stepHash := strings.ToLower(strings.TrimSpace(step.HashHex)) + if stepHash == "" { + return false, "missing hash_hex" + } + expectedCount := step.SiblingCount + if expectedCount == 0 { + expectedCount = 256 + } + if len(step.SiblingHashes) != expectedCount { + return false, "sibling_count mismatch" + } + if len(step.SiblingHashes) == 0 { + return false, "missing sibling hashes" + } + rollup, errMsg := hashHexConcat(step.SiblingHashes, hashAlgo) + if errMsg != "" { + return false, errMsg + } + if !strings.EqualFold(hex.EncodeToString(rollup), stepHash) { + return false, "level rollup mismatch" + } + } + + last := strings.ToLower(strings.TrimSpace(path.Proof[len(path.Proof)-1].HashHex)) + if last == "" { + return false, "missing final hash" + } + if !strings.EqualFold(last, rootHash) { + return false, "root hash mismatch" + } + return true, "" +} + +// VerifyKayrosRecordWithProof verifies the record and its proof path. +func VerifyKayrosRecordWithProof(record *KayrosRecord, prev *KayrosRecord, proof *ProofPathData, hashAlgo string) (bool, string) { + if ok, errMsg := VerifyKayrosRecord(record, prev, hashAlgo, nil, KayrosConfig{}); !ok { + return false, errMsg + } + if proof == nil { + return false, "missing proof path" + } + if !strings.EqualFold(proof.HashItemHex, record.HashItemHex) { + return false, "hash_item mismatch" + } + if !strings.EqualFold(proof.DataTypeHex, record.DataTypeHex) { + return false, "data_type mismatch" + } + if !strings.EqualFold(proof.DataItemHex, record.DataItemHex) { + return false, "data_item mismatch" + } + if ok, errMsg := VerifyProofPath(proof, hashAlgo); !ok { + return false, errMsg + } + return true, "" +} + +// VerifyKayrosRecord runs hash, chain, uuid, and level checks. +func VerifyKayrosRecord(record *KayrosRecord, prev *KayrosRecord, hashAlgo string, levelProofs []LevelProof, cfg KayrosConfig) (bool, string) { + if ok, errMsg := VerifyRecordHash(record, hashAlgo); !ok { + return false, errMsg + } + if ok, errMsg := VerifyRecordChainLink(record, prev); !ok { + return false, errMsg + } + if ok, errMsg := VerifyRecordUUID(record); !ok { + return false, errMsg + } + + for _, proof := range levelProofs { + if ok, errMsg := VerifyLevelProof(cfg, proof, hashAlgo); !ok { + return false, errMsg + } + } + return true, "" +} + +func hashBytes(data []byte, hashAlgo string) ([]byte, string) { + hashAlgo = strings.ToLower(strings.TrimSpace(hashAlgo)) + if hashAlgo == "" || hashAlgo == "sha256" { + return wasmx.Sha256(data), "" + } + if hashAlgo == "keccak256" { + return wasmx.Keccak256(data), "" + } + return nil, "unsupported hash algorithm" +} + +func hashHexConcat(hashes []string, hashAlgo string) ([]byte, string) { + payload := make([]byte, 0) + for _, h := range hashes { + bz, err := decodeHex(h) + if err != nil { + return nil, "invalid hash hex" + } + payload = append(payload, bz...) + } + return hashBytes(payload, hashAlgo) +} + +func decodeHex(value string) ([]byte, error) { + return hex.DecodeString(strings.TrimSpace(value)) +} + +func decodeHexOrEmpty(value string) ([]byte, error) { + value = strings.TrimSpace(value) + if value == "" { + return []byte{}, nil + } + return hex.DecodeString(value) +} diff --git a/tests/testdata/tinygo/wasmx-kayros-verifier/lib/types.go b/tests/testdata/tinygo/wasmx-kayros-verifier/lib/types.go new file mode 100644 index 00000000..44600033 --- /dev/null +++ b/tests/testdata/tinygo/wasmx-kayros-verifier/lib/types.go @@ -0,0 +1,229 @@ +package lib + +import ( + wasmx "github.com/loredanacirstea/wasmx-env/lib" +) + +const ( + MODULE_NAME = "kayros_verifier" +) + +// KayrosConfig holds the configuration for Kayros API client +type KayrosConfig struct { + ApiBaseUrl string `json:"api_base_url"` + ApiUserKey string `json:"api_user_key"` +} + +// KayrosRecord represents a record in the Kayros database +type KayrosRecord struct { + DataType string `json:"data_type"` + DataTypeHex string `json:"data_type_hex"` + DataItemHex string `json:"data_item_hex"` + UuidHex string `json:"uuid_hex"` + HashItemHex string `json:"hash_item_hex"` + PrevHashHex string `json:"prev_hash_hex,omitempty"` + HashType string `json:"hash_type"` + Timestamp string `json:"timestamp"` +} + +// KayrosApiResponse is the common response structure from Kayros API +type KayrosApiResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +// KayrosRecordResponse is the response for single record queries +type KayrosRecordResponse struct { + KayrosApiResponse + Data KayrosRecord `json:"data"` +} + +// KayrosRecordsData is the data structure for multiple record queries +type KayrosRecordsData struct { + Count int `json:"count"` + Limit int `json:"limit"` + Records []KayrosRecord `json:"records"` +} + +// KayrosRecordsResponse is the response for multiple record queries +type KayrosRecordsResponse struct { + KayrosApiResponse + Data KayrosRecordsData `json:"data"` +} + +// KayrosRegistrationRequest is the request body for POST /api/grpc/single-hash +type KayrosRegistrationRequest struct { + DataType wasmx.HexString `json:"data_type"` + DataItem wasmx.HexString `json:"data_item"` +} + +// KayrosRegistrationResponse is the response from the registration API +type KayrosRegistrationResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + DataType string `json:"data_type"` + DataItem string `json:"data_item"` + ComputedHash wasmx.HexString `json:"computed_hash_hex"` + TimeUUID wasmx.HexString `json:"timeuuid_hex"` + DataTypeHex wasmx.HexString `json:"data_type_hex"` + DataItemHex wasmx.HexString `json:"data_item_hex"` +} + +type KayrosRegistrationResponseWrap struct { + KayrosApiResponse + Data KayrosRegistrationResponse `json:"data"` +} + +type LevelHashEntry struct { + Position int `json:"position"` + HashHex string `json:"hash_hex"` + UuidHex string `json:"uuid_hex"` +} + +type LevelHashData struct { + Level int `json:"level"` + Position int `json:"position"` + HashHex string `json:"hash_hex"` +} + +type LevelHashResponse struct { + KayrosApiResponse + Data LevelHashData `json:"data"` +} + +type LevelRangeData struct { + Level int `json:"level"` + Count int `json:"count"` + Entries []LevelHashEntry `json:"entries"` +} + +type LevelRangeResponse struct { + KayrosApiResponse + Data LevelRangeData `json:"data"` +} + +type ProofPathEntry struct { + Level int `json:"level"` + Position int `json:"position"` + HashHex string `json:"hash_hex"` + IndexInBlock int `json:"index_in_block"` + SiblingHashes []string `json:"sibling_hashes"` + SiblingCount int `json:"sibling_count"` +} + +type ProofPathData struct { + HashItemHex string `json:"hash_item_hex"` + DataTypeHex string `json:"data_type_hex"` + DataItemHex string `json:"data_item_hex"` + RecordIndex int `json:"record_index"` + Proof []ProofPathEntry `json:"proof"` + RootHashHex string `json:"root_hash_hex"` +} + +type ProofPathResponse struct { + KayrosApiResponse + Data ProofPathData `json:"data"` +} + +type VerifyResponse struct { + Ok bool `json:"ok"` + Error string `json:"error,omitempty"` +} + +type VerifyProofRequest struct { + Data []byte `json:"data"` + DataType []byte `json:"data_type"` + HashAlgo string `json:"hash_algo"` + ApiBaseUrl string `json:"api_base_url"` + ApiUserKey string `json:"api_user_key"` +} + +type VerifyProofWithInclusionRequest struct { + Data []byte `json:"data"` + DataType []byte `json:"data_type"` + HashAlgo string `json:"hash_algo"` + TrustedRootHash string `json:"trusted_root_hash"` + TrustedLevel int `json:"trusted_level"` + TrustedPosition int `json:"trusted_position"` + ApiBaseUrl string `json:"api_base_url"` + ApiUserKey string `json:"api_user_key"` +} + +type VerifyProofHashRequest struct { + DataType []byte `json:"data_type"` + DataHash []byte `json:"data_hash"` + ApiBaseUrl string `json:"api_base_url"` + ApiUserKey string `json:"api_user_key"` +} + +type VerifyProofHashWithInclusionRequest struct { + DataType []byte `json:"data_type"` + DataHash []byte `json:"data_hash"` + HashAlgo string `json:"hash_algo"` + TrustedRootHash string `json:"trusted_root_hash"` + TrustedLevel int `json:"trusted_level"` + TrustedPosition int `json:"trusted_position"` + ApiBaseUrl string `json:"api_base_url"` + ApiUserKey string `json:"api_user_key"` +} + +type VerifyRecordHashRequest struct { + Record KayrosRecord `json:"record"` + HashAlgo string `json:"hash_algo"` +} + +type VerifyRecordChainLinkRequest struct { + Record KayrosRecord `json:"record"` + Prev KayrosRecord `json:"prev"` +} + +type VerifyRecordTimestampRequest struct { + Record KayrosRecord `json:"record"` +} + +type VerifyRecordUUIDRequest struct { + Record KayrosRecord `json:"record"` +} + +type VerifyLevelProofRequest struct { + Proof LevelProof `json:"proof"` + HashAlgo string `json:"hash_algo"` + ApiBaseUrl string `json:"api_base_url"` + ApiUserKey string `json:"api_user_key"` +} + +type VerifyProofPathRequest struct { + Proof ProofPathData `json:"proof"` + HashAlgo string `json:"hash_algo"` +} + +type VerifyKayrosRecordRequest struct { + Record KayrosRecord `json:"record"` + Prev KayrosRecord `json:"prev"` + HashAlgo string `json:"hash_algo"` + LevelProofs []LevelProof `json:"level_proofs"` + ApiBaseUrl string `json:"api_base_url"` + ApiUserKey string `json:"api_user_key"` +} + +type VerifyKayrosRecordWithProofRequest struct { + Record KayrosRecord `json:"record"` + Prev KayrosRecord `json:"prev"` + Proof ProofPathData `json:"proof"` + HashAlgo string `json:"hash_algo"` +} + +type Calldata struct { + VerifyProof *VerifyProofRequest `json:"verify_proof,omitempty"` + VerifyProofWithInclusion *VerifyProofWithInclusionRequest `json:"verify_proof_with_inclusion,omitempty"` + VerifyProofHash *VerifyProofHashRequest `json:"verify_proof_hash,omitempty"` + VerifyProofHashWithInclusion *VerifyProofHashWithInclusionRequest `json:"verify_proof_hash_with_inclusion,omitempty"` + VerifyRecordHash *VerifyRecordHashRequest `json:"verify_record_hash,omitempty"` + VerifyRecordChainLink *VerifyRecordChainLinkRequest `json:"verify_record_chain_link,omitempty"` + VerifyRecordTimestamp *VerifyRecordTimestampRequest `json:"verify_record_timestamp,omitempty"` + VerifyRecordUUID *VerifyRecordUUIDRequest `json:"verify_record_uuid,omitempty"` + VerifyLevelProof *VerifyLevelProofRequest `json:"verify_level_proof,omitempty"` + VerifyProofPath *VerifyProofPathRequest `json:"verify_proof_path,omitempty"` + VerifyKayrosRecord *VerifyKayrosRecordRequest `json:"verify_kayros_record,omitempty"` + VerifyKayrosRecordWithProof *VerifyKayrosRecordWithProofRequest `json:"verify_kayros_record_with_proof,omitempty"` +} diff --git a/tests/testdata/tinygo/wasmx-kayros-verifier/lib/utils.go b/tests/testdata/tinygo/wasmx-kayros-verifier/lib/utils.go new file mode 100644 index 00000000..097ec4f9 --- /dev/null +++ b/tests/testdata/tinygo/wasmx-kayros-verifier/lib/utils.go @@ -0,0 +1,71 @@ +package lib + +import ( + "encoding/hex" + "fmt" + "time" + + wasmx "github.com/loredanacirstea/wasmx-env/lib" +) + +func LoggerInfo(msg string, parts []string) { + wasmx.LoggerInfo(MODULE_NAME, msg, parts) +} + +func LoggerError(msg string, parts []string) { + wasmx.LoggerError(MODULE_NAME, msg, parts) +} + +func LoggerDebug(msg string, parts []string) { + wasmx.LoggerDebug(MODULE_NAME, msg, parts) +} + +func LoggerDebugExtended(msg string, parts []string) { + wasmx.LoggerDebugExtended(MODULE_NAME, msg, parts) +} + +// TimeuuidHexToTimestamp extracts timestamp from a Kayros TimeUUID hex string +// TimeUUID format (version 1): time-low(4) time-mid(2) time-high-and-version(2) clock-seq(2) node(6) +// Returns RFC3339Nano formatted timestamp string +func TimeuuidHexToTimestamp(timeuuidHex string) (string, error) { + // Decode hex string to bytes + uuidBytes, err := hex.DecodeString(timeuuidHex) + if err != nil { + return "", fmt.Errorf("invalid hex: %w", err) + } + + if len(uuidBytes) != 16 { + return "", fmt.Errorf("UUIDs must be exactly 16 bytes long") + } + + // Extract timestamp from TimeUUID (version 1) + // UUIDs are stored in big-endian (network byte order) + // The timestamp is split across time_low (bytes 0-3), time_mid (bytes 4-5), + // and time_hi_and_version (bytes 6-7) + + // Read time_low (32 bits, big-endian) + timeLow := uint64(uuidBytes[0])<<24 | uint64(uuidBytes[1])<<16 | + uint64(uuidBytes[2])<<8 | uint64(uuidBytes[3]) + + // Read time_mid (16 bits, big-endian) + timeMid := uint64(uuidBytes[4])<<8 | uint64(uuidBytes[5]) + + // Read time_hi_and_version (16 bits, big-endian), mask out version bits (top 4 bits) + timeHi := (uint64(uuidBytes[6])<<8 | uint64(uuidBytes[7])) & 0x0FFF + + // Reconstruct the 60-bit timestamp + // timestamp = time_low + (time_mid << 32) + (time_hi << 48) + timestamp := timeLow | (timeMid << 32) | (timeHi << 48) + + // UUID timestamp is in 100-nanosecond intervals since UUID epoch (1582-10-15) + // Convert to Unix time (nanoseconds since 1970-01-01) + const gregorianEpoch = 122192928000000000 // 100-ns intervals between UUID epoch and Unix epoch + + unixNanos := int64((timestamp - gregorianEpoch) * 100) + + // Convert to time.Time + t := time.Unix(0, unixNanos) + + // Format as RFC3339Nano + return t.UTC().Format(time.RFC3339Nano), nil +} diff --git a/tests/testdata/tinygo/wasmx-kayrosp2p-lib/README.md b/tests/testdata/tinygo/wasmx-kayrosp2p-lib/README.md new file mode 100644 index 00000000..94114b5b --- /dev/null +++ b/tests/testdata/tinygo/wasmx-kayrosp2p-lib/README.md @@ -0,0 +1,24 @@ +# Kayros Consensus + +A new type of consensus based on our Kayros indexer. Each node will be initialized with a Kayros gateway url. The Kayros indexer is the one who orders transactions and gives them a timestamp. So, the node/validator sends the user transaction to Kayros and gets a Kayros record that is should also include in the block. And will use this Kayros order to order the transactions in a block. + +Nodes therefore produce blocks continuously (they finalized blocks continuously). There is no Leader, because Kayros is the only source of truth. We will only check that nodes are in sync by propagating the block hashes and then comparing them per block. If a node sees > x% (e.g. 50%) of nodes having a different block hash, it will revert its blocks and take them from the validators with the majority. And then continue the protocol. + +## Protocol + +User sends the transaction to the node. The node adds the transaction to the mempool, registers it with Kayros if it was not registered. And forwards it to other nodes. + +Block production: +- get max `x` records with data_type `wasmx_` since the previous record hash +- match records with mempool transactions +- if a record does not have a transaction, ask the other validators for the missing txs +- if tx does not appear in x time, produce block without it +- after block is produced, the block hash and txhash list is sent to the other nodes + +Block check: +- block hashes are coming in our KAYROS_BLOCKHASH_CHATROOM and we keep them in a mapping block_number => []BlockHash , with hash and validator address; when we have > 50% we check if the hash matches our hash, if not, we rollback the blocks until that block and ask the validators with correct hash for the block. +- only after this check do we consider the block as stable (cannot be rolled back), even though the probability of producing a bad block is low (only when txs don't arrive in the mempool due to a bad actor) + +## agents info + +FinalizeBlock is part of the process to commit a block. we run this as part of block production. so we run this automatically, for every block, regardless of other nodes/validators. but in parallel, we take messages from the other nodes/validators who send their block hashes and check if those block hashes match ours; this is where our thresholds come in. if we see other validators sending a different blockhash (the message needs to be signed by the validator and we need to verify the signature), then if > CTX_THRESHOLD_COMMIT, we will rollback all the blocks until the offending block (including it) and ask validators with the correct hash for the blocks; these receiveCommit messages, with block hashes keep comming, so when we get a > CTX_THRESHOLD_FINALIZE it just means that the block for sure will not rollback, so we will no longer be concerned with receiveCommit messages about that block; but FinalizeBlock has been already called when the block was produced in the first place. it is different from RAFT, because RAFT and tendermint have an entire voting process and usually split block preparation, dissemination and finalization into multiple phases. but here, we fully produce and finalize the block locally, without voting. and we correct any issues post-block finalization, because they should be very rare. it is fast consensus, because ordering is done by kayros. diff --git a/tests/testdata/tinygo/wasmx-kayrosp2p-lib/cmd/main.go b/tests/testdata/tinygo/wasmx-kayrosp2p-lib/cmd/main.go new file mode 100644 index 00000000..1012f3e8 --- /dev/null +++ b/tests/testdata/tinygo/wasmx-kayrosp2p-lib/cmd/main.go @@ -0,0 +1,257 @@ +package main + +import ( + "encoding/json" + + wasmx "github.com/loredanacirstea/wasmx-env/lib" + fsm "github.com/loredanacirstea/wasmx-fsm/lib" + lib "github.com/loredanacirstea/wasmx-kayrosp2p-lib/lib" + raft "github.com/loredanacirstea/wasmx-raft-lib/lib" + raftp2p "github.com/loredanacirstea/wasmx-raftp2p-lib/lib" +) + +//go:wasm-module wasmx +//export memory_ptrlen_i64_1 +func Memory_ptrlen_i64_1() {} + +//go:wasm-module wasmx +//export wasmx_env_i64_2 +func Wasmx_env_i64_2() {} + +//go:wasm-module consensus +//export wasmx_consensus_json_i64_1 +func Wasmx_consensus_json_i64_1() {} + +//go:wasm-module wasmxcore +//export wasmx_env_core_i64_1 +func Wasmx_env_core_i64_1() {} + +//go:wasm-module crosschain +//export wasmx_crosschain_json_i64_1 +func Wasmx_crosschain_json_i64_1() {} + +//go:wasm-module multichain +//export wasmx_multichain_json_i64_1 +func Wasmx_multichain_json_i64_1() {} + +//go:wasm-module p2p +//export wasmx_p2p_json_i64_1 +func Wasmx_p2p_json_i64_1() {} + +//go:wasm-module httpclient +//export wasmx_httpclient_i64_1 +func Wasmx_httpclient_i64_1() {} + +//go:wasm-module wasmxcore +//export wasmx_nondeterministic_1 +func Wasmx_nondeterministic_1() {} + +//go:wasm-module wasmx-raftp2p +//export instantiate +func Instantiate() {} + +// note, we cannot instantiate with storage this library +// because it will read from the main consensus contract + +func main() { + // Only internal + wasmx.OnlyInternal(raftp2p.MODULE_NAME, "") + + databz := wasmx.GetCallData() + var calld fsm.ExternalActionCallData + if err := json.Unmarshal(databz, &calld); err != nil { + raftp2p.Revert("invalid call data: " + err.Error() + ": " + string(databz)) + return + } + + // Helper to read params from event + get := func(key string) string { + for _, p := range calld.Event.Params { + if p.Key == key { + return p.Value + } + } + for _, p := range calld.Params { + if p.Key == key { + return p.Value + } + } + return "" + } + + // Route methods + switch calld.Method { + case "ifNodeIsValidator": + ok, err := raftp2p.IfNodeIsValidator(calld.Params, calld.Event) + if err != nil { + raftp2p.Revert(err.Error()) + return + } + res := raft.WrapGuard(ok) + wasmx.Finish(res) + return + case "ifNewTransaction": + ok, err := raftp2p.IfNewTransaction(calld.Params, calld.Event) + if err != nil { + raftp2p.Revert(err.Error()) + return + } + res := raft.WrapGuard(ok) + wasmx.Finish(res) + return + case "setupNode": + if err := raftp2p.SetupNode(calld.Params, calld.Event); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "connectPeers": + st, err := raft.GetCurrentState() + if err != nil { + raftp2p.Revert(err.Error()) + return + } + if err := raftp2p.ConnectPeersInternal(raftp2p.GetProtocolIdFromState(st)); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "connectRooms": + if err := raftp2p.ConnectRooms(); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "registerWithKayros": + _, err := lib.RegisterWithKayros(calld.Params, calld.Event) + if err != nil { + raftp2p.Revert(err.Error()) + return + } + case "getKayrosTxs": + err := lib.GetKayrosTxs(calld.Params, calld.Event) + if err != nil { + raftp2p.Revert(err.Error()) + return + } + case "forwardMsgToChat": + if err := raftp2p.ForwardMsgToChat(calld.Params, calld.Event); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "requestBlockSync": + if err := raftp2p.RequestBlockSync(); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "receiveStateSyncRequest": + entry := get("entry") + sig := get("signature") + sender := get("sender") + if err := raftp2p.ReceiveStateSyncRequest(entry, sig, wasmx.Bech32String(sender)); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "receiveStateSyncResponse": + entry := get("entry") + sender := get("sender") + if err := raftp2p.ReceiveStateSyncResponse(entry, sender); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "receiveUpdateNodeResponse": + entry := get("entry") + sig := get("signature") + sender := get("sender") + if err := raftp2p.ReceiveUpdateNodeResponse(entry, sig, wasmx.Bech32String(sender)); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "receiveCommit": + if err := lib.ReceiveCommit(calld.Params, calld.Event); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "receiveMissingTransactions": + if err := lib.ReceiveMissingTransactions(calld.Params, calld.Event); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "receiveMissingTransactionsRequest": + if err := lib.ReceiveMissingTransactionsRequest(calld.Params, calld.Event); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "ifAllTransactions": + ok := lib.IfAllTransactions(nil, calld.Event) + res := raft.WrapGuard(ok) + wasmx.Finish(res) + return + case "sendNewTransactionResponse": + if err := raft.SendNewTransactionResponse(nil, fsm.EventObject{}); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "addToMempool": + if err := raftp2p.AddToMempool(calld.Params, calld.Event); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "updateNodeAndReturn": + if err := raftp2p.UpdateNodeAndReturn(calld.Params, calld.Event); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "registerValidatorWithNetwork": + if err := raftp2p.RegisterValidatorWithNetwork(nil, fsm.EventObject{}); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "setup": + if err := raft.Setup(calld.Params, calld.Event); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "bootstrapAfterStateSync": + if err := raft.BootstrapAfterStateSync(calld.Params, calld.Event); err != nil { + raftp2p.Revert("bootstrapAfterStateSync failed: " + err.Error()) + return + } + case "commitAfterStateSync": + if err := raft.CommitAfterStateSync(calld.Params, calld.Event); err != nil { + raftp2p.Revert("commitAfterStateSync failed: " + err.Error()) + return + } + case "VerifyCommitLight": + if err := lib.VerifyCommitLight(calld.Params, calld.Event); err != nil { + raftp2p.Revert("VerifyCommitLight failed: " + err.Error()) + return + } + case "rollback": + if err := raft.Rollback(calld.Params, calld.Event); err != nil { + raftp2p.Revert("Rollback failed: " + err.Error()) + return + } + case "commitBlock": + if err := lib.CommitBlock(calld.Params, calld.Event); err != nil { + raftp2p.Revert("commitBlock failed: " + err.Error()) + return + } + case "sendCommit": + if err := lib.SendCommit(calld.Params, calld.Event); err != nil { + raftp2p.Revert("sendCommit failed: " + err.Error()) + return + } + case "cancelActiveIntervals": + // Cancel active intervals - this is handled by the FSM runtime + // Just acknowledge the action + lib.LoggerDebug("cancelActiveIntervals called", nil) + case "requestValidatorNodeInfoIfSynced": + // Request validator node info after state sync - stub for now + lib.LoggerDebug("requestValidatorNodeInfoIfSynced called", nil) + case "receiveUpdateNodeRequest": + raftp2p.Revert("receiveUpdateNodeRequest not implemented") + return + default: + wasmx.Revert(append([]byte("invalid function call data: "), databz...)) + return + } + wasmx.Finish(wasmx.GetFinishData()) +} diff --git a/tests/testdata/tinygo/wasmx-kayrosp2p-lib/diagram/def.json b/tests/testdata/tinygo/wasmx-kayrosp2p-lib/diagram/def.json new file mode 100644 index 00000000..1bcf41d5 --- /dev/null +++ b/tests/testdata/tinygo/wasmx-kayrosp2p-lib/diagram/def.json @@ -0,0 +1 @@ +{"context":[{"key":"blockTimeout","value":"timeoutCommit"},{"key":"max_tx_bytes","value":65536},{"key":"timeoutCommit","value":4000},{"key":"max_block_gas","value":"20000000"},{"key":"timeoutMissingTxs","value":4000},{"key":"kayros_base_url","value":""},{"key":"kayros_user_key","value":""},{"key":"threshold_commit","value":51},{"key":"threshold_finalize","value":75},{"key":"genesis_uuid","value":""},{"key":"data_type_id","value":""},{"key":"max_block_tx","value":"30"}],"id":"Kayros-P2P-1","initial":"uninitialized","states":[{"name":"uninitialized","after":[],"always":[],"on":[{"name":"initialize","transitions":[{"target":"#Kayros-P2P-1.initialized","guard":null,"actions":[],"meta":[]}]}],"entry":[],"exit":[],"initial":"","states":[]},{"name":"initialized","after":[],"always":[],"on":[],"entry":[],"exit":[],"initial":"unstarted","states":[{"name":"unstarted","after":[],"always":[],"on":[{"name":"setupNode","transitions":[{"target":"#Kayros-P2P-1.initialized.unstarted","guard":null,"actions":[{"type":"setupNode","params":[]}],"meta":[]}]},{"name":"prestart","transitions":[{"target":"#Kayros-P2P-1.initialized.prestart","guard":null,"actions":[],"meta":[]}]},{"name":"setup","transitions":[{"target":"#Kayros-P2P-1.initialized.unstarted","guard":null,"actions":[{"type":"setup","params":[]}],"meta":[]}]},{"name":"start","transitions":[{"target":"#Kayros-P2P-1.initialized.started","guard":null,"actions":[{"type":"connectPeers","params":[]},{"type":"connectRooms","params":[]},{"type":"requestBlockSync","params":[]}],"meta":[]}]}],"entry":[],"exit":[],"initial":"","states":[]},{"name":"prestart","after":[{"name":"500","transitions":[{"target":"#Kayros-P2P-1.initialized.started","guard":null,"actions":[],"meta":[]}]}],"always":[],"on":[],"entry":[],"exit":[],"initial":"","states":[]},{"name":"started","after":[],"always":[],"on":[{"name":"newTransaction","transitions":[{"target":"","guard":{"type":"ifNewTransaction","params":[]},"actions":[{"type":"addToMempool","params":[]},{"type":"registerWithKayros","params":[]},{"type":"sendNewTransactionResponse","params":[]},{"type":"forwardMsgToChat","params":[{"key":"protocolId","value":"mempool"}]}],"meta":[]}]},{"name":"receiveStateSyncRequest","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveStateSyncRequest","params":[]}],"meta":[]}]},{"name":"updateNode","transitions":[{"target":"","guard":null,"actions":[{"type":"updateNodeAndReturn","params":[]}],"meta":[]}]}],"entry":[],"exit":[],"initial":"Node","states":[{"name":"Node","after":[],"always":[{"name":"always","transitions":[{"target":"#Kayros-P2P-1.initialized.started.Validator","actions":[{"type":"registerValidatorWithNetwork","params":[]}],"guard":{"type":"ifNodeIsValidator","params":[]},"meta":[]}]}],"on":[{"name":"becomeValidator","transitions":[{"target":"#Kayros-P2P-1.initialized.started.Validator","guard":null,"actions":[{"type":"registerValidatorWithNetwork","params":[]},{"type":"requestBlockSync","params":[]}],"meta":[]}]},{"name":"receiveStateSyncResponse","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveStateSyncResponse","params":[]}],"meta":[]}]},{"name":"start","transitions":[{"target":"","guard":null,"actions":[{"type":"connectPeers","params":[]},{"type":"connectRooms","params":[]},{"type":"requestBlockSync","params":[]}],"meta":[]}]},{"name":"receiveCommit","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveCommit","params":[]}],"meta":[]}]},{"name":"receiveUpdateNodeResponse","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveUpdateNodeResponse","params":[]}],"meta":[]}]}],"entry":[],"exit":[],"initial":"","states":[]},{"name":"Validator","after":[],"always":[],"on":[{"name":"stop","transitions":[{"target":"#Kayros-P2P-1.stopped","guard":null,"actions":[],"meta":[]}]},{"name":"receiveUpdateNodeResponse","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveUpdateNodeResponse","params":[]}],"meta":[]}]},{"name":"start","transitions":[{"target":"#Kayros-P2P-1.initialized.started.Validator","guard":null,"actions":[{"type":"connectPeers","params":[]},{"type":"connectRooms","params":[]},{"type":"registerValidatorWithNetwork","params":[]},{"type":"requestBlockSync","params":[]}],"meta":[]}]},{"name":"receiveCommit","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveCommit","params":[]}],"meta":[]}]},{"name":"receiveUpdateNodeRequest","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveUpdateNodeRequest","params":[]}],"meta":[]}]},{"name":"receiveStateSyncResponse","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveStateSyncResponse","params":[]},{"type":"requestValidatorNodeInfoIfSynced","params":[]}],"meta":[]}]}],"entry":[],"exit":[],"initial":"active","states":[{"name":"active","after":[{"name":"timeoutMissingTxs","transitions":[{"target":"#Kayros-P2P-1.initialized.started.Validator.propose","guard":null,"actions":[],"meta":[]}]}],"always":[{"name":"always","transitions":[{"target":"#Kayros-P2P-1.initialized.started.Validator.propose","guard":{"type":"ifAllTransactions","params":[]},"actions":[],"meta":[]}]}],"on":[{"name":"receiveMissingTransactions","transitions":[{"target":"#Kayros-P2P-1.initialized.started.Validator.active","guard":null,"actions":[{"type":"receiveMissingTransactions","params":[]}],"meta":[]}]}],"entry":[{"type":"getKayrosTxs","params":[]},{"type":"cancelActiveIntervals","params":[{"key":"after","value":"timeoutMissingTxs"}]}],"exit":[],"initial":"","states":[]},{"name":"propose","after":[{"name":"timeoutCommit","transitions":[{"target":"#Kayros-P2P-1.initialized.started.Validator.active","guard":null,"actions":[],"meta":[]}]}],"always":[],"on":[],"entry":[{"type":"commitBlock","params":[]},{"type":"sendCommit","params":[]},{"type":"cancelActiveIntervals","params":[{"key":"after","value":"timeoutCommit"}]}],"exit":[],"initial":"","states":[]}]}]}]},{"name":"stopped","after":[],"always":[],"on":[{"name":"restart","transitions":[{"target":"#Kayros-P2P-1.initialized.unstarted","guard":null,"actions":[],"meta":[]}]}],"entry":[],"exit":[],"initial":"","states":[]}]} diff --git a/tests/testdata/tinygo/wasmx-kayrosp2p-lib/diagram/def.ts b/tests/testdata/tinygo/wasmx-kayrosp2p-lib/diagram/def.ts new file mode 100644 index 00000000..3141e24a --- /dev/null +++ b/tests/testdata/tinygo/wasmx-kayrosp2p-lib/diagram/def.ts @@ -0,0 +1,385 @@ +// @ts-nocheck +import { createMachine } from "xstate"; + +export const machine = createMachine({ + context: { + blockTimeout: "timeoutCommit", + max_tx_bytes: 65536, + timeoutCommit: 4000, + max_block_gas: "20000000", + timeoutMissingTxs: 4000, + kayros_base_url: "", + kayros_user_key: "", + threshold_commit: 51, + threshold_finalize: 75, + genesis_uuid: "", + data_type_id: "", + max_block_tx: "", + }, + id: "Kayros-P2P-1", + initial: "uninitialized", + states: { + uninitialized: { + on: { + initialize: { + target: "initialized", + }, + }, + }, + initialized: { + initial: "unstarted", + states: { + unstarted: { + on: { + setupNode: { + target: "unstarted", + actions: { + type: "setupNode", + }, + }, + prestart: { + target: "prestart", + }, + setup: { + target: "unstarted", + actions: { + type: "setup", + }, + }, + start: { + target: "started", + actions: [ + { + type: "connectPeers", + }, + { + type: "connectRooms", + }, + { + type: "requestBlockSync", + }, + ], + }, + }, + }, + prestart: { + after: { + "500": { + target: "started", + }, + }, + }, + started: { + initial: "Node", + on: { + newTransaction: { + actions: [ + { + type: "addToMempool", + }, + { + type: "registerWithKayros", + }, + { + type: "sendNewTransactionResponse", + }, + { + type: "forwardMsgToChat", + params: { + protocolId: "mempool", + }, + }, + ], + guard: { + type: "ifNewTransaction", + }, + }, + receiveStateSyncRequest: { + actions: { + type: "receiveStateSyncRequest", + }, + }, + updateNode: { + actions: { + type: "updateNodeAndReturn", + }, + }, + }, + states: { + Node: { + on: { + becomeValidator: { + target: "Validator", + actions: [ + { + type: "registerValidatorWithNetwork", + }, + { + type: "requestBlockSync", + }, + ], + }, + receiveStateSyncResponse: { + actions: { + type: "receiveStateSyncResponse", + }, + }, + start: { + actions: [ + { + type: "connectPeers", + }, + { + type: "connectRooms", + }, + { + type: "requestBlockSync", + }, + ], + }, + receiveCommit: { + actions: { + type: "receiveCommit", + }, + }, + receiveUpdateNodeResponse: { + actions: { + type: "receiveUpdateNodeResponse", + }, + }, + }, + always: { + target: "Validator", + actions: { + type: "registerValidatorWithNetwork", + }, + guard: { + type: "ifNodeIsValidator", + }, + }, + }, + Validator: { + initial: "active", + on: { + stop: { + target: "#Kayros-P2P-1.stopped", + }, + receiveUpdateNodeResponse: { + actions: { + type: "receiveUpdateNodeResponse", + }, + }, + start: { + target: "Validator", + actions: [ + { + type: "connectPeers", + }, + { + type: "connectRooms", + }, + { + type: "registerValidatorWithNetwork", + }, + { + type: "requestBlockSync", + }, + ], + }, + receiveCommit: { + actions: { + type: "receiveCommit", + }, + description: + "Receive the block hashes from other validators and finalize the block. Rollback if 2/3 with another hash.", + }, + receiveUpdateNodeRequest: { + actions: { + type: "receiveUpdateNodeRequest", + }, + }, + receiveStateSyncResponse: { + actions: [ + { + type: "receiveStateSyncResponse", + }, + { + type: "requestValidatorNodeInfoIfSynced", + }, + ], + }, + }, + states: { + active: { + on: { + receiveMissingTransactions: { + target: "active", + actions: { + type: "receiveMissingTransactions", + }, + }, + }, + after: { + timeoutMissingTxs: { + target: "propose", + }, + }, + always: { + target: "propose", + guard: { + type: "ifAllTransactions", + }, + }, + entry: [ + { + type: "getKayrosTxs", + }, + { + type: "cancelActiveIntervals", + params: { + after: "timeoutMissingTxs", + }, + }, + ], + }, + propose: { + after: { + timeoutCommit: { + target: "active", + }, + }, + entry: [ + { + type: "commitBlock", + }, + { + type: "sendCommit", + }, + { + type: "cancelActiveIntervals", + params: { + after: "timeoutCommit", + }, + }, + ], + }, + }, + }, + }, + }, + }, + }, + stopped: { + on: { + restart: { + target: "#Kayros-P2P-1.initialized.unstarted", + }, + }, + }, + }, +}).withConfig({ + actions: { + getKayrosTxs: function (context, event) { + // Add your action code here + // ... + }, + cancelActiveIntervals: function (context, event) { + // Add your action code here + // ... + }, + commitBlock: function (context, event) { + // Add your action code here + // ... + }, + sendCommit: function (context, event) { + // Add your action code here + // ... + }, + setupNode: function (context, event) { + // Add your action code here + // ... + }, + addToMempool: function (context, event) { + // Add your action code here + // ... + }, + registerWithKayros: function (context, event) { + // Add your action code here + // ... + }, + sendNewTransactionResponse: function (context, event) { + // Add your action code here + // ... + }, + forwardMsgToChat: function (context, event) { + // Add your action code here + // ... + }, + setup: function (context, event) { + // Add your action code here + // ... + }, + receiveUpdateNodeResponse: function (context, event) { + // Add your action code here + // ... + }, + receiveStateSyncRequest: function (context, event) { + // Add your action code here + // ... + }, + updateNodeAndReturn: function (context, event) { + // Add your action code here + // ... + }, + registerValidatorWithNetwork: function (context, event) { + // Add your action code here + // ... + }, + requestBlockSync: function (context, event) { + // Add your action code here + // ... + }, + receiveStateSyncResponse: function (context, event) { + // Add your action code here + // ... + }, + connectPeers: function (context, event) { + // Add your action code here + // ... + }, + connectRooms: function (context, event) { + // Add your action code here + // ... + }, + receiveCommit: function (context, event) { + // Add your action code here + // ... + }, + receiveUpdateNodeRequest: function (context, event) { + // Add your action code here + // ... + }, + requestValidatorNodeInfoIfSynced: function (context, event) { + // Add your action code here + // ... + }, + receiveMissingTransactions: function (context, event) { + // Add your action code here + // ... + }, + }, + guards: { + ifNewTransaction: function (context, event) { + // Add your guard condition here + return true; + }, + ifNodeIsValidator: function (context, event) { + // Add your guard condition here + return true; + }, + ifAllTransactions: function (context, event) { + // Add your guard condition here + return true; + }, + }, +}); diff --git a/tests/testdata/tinygo/wasmx-kayrosp2p-lib/go.mod b/tests/testdata/tinygo/wasmx-kayrosp2p-lib/go.mod new file mode 100644 index 00000000..052ade7a --- /dev/null +++ b/tests/testdata/tinygo/wasmx-kayrosp2p-lib/go.mod @@ -0,0 +1,71 @@ +module github.com/loredanacirstea/wasmx-kayrosp2p-lib + +go 1.24 + +toolchain go1.24.4 + +require github.com/loredanacirstea/wasmx-env v0.0.0 + +require github.com/loredanacirstea/wasmx-fsm v0.0.0 + +require github.com/loredanacirstea/wasmx-env-core v0.0.0 // indirect + +require github.com/loredanacirstea/wasmx-env-consensus v0.0.0 + +require github.com/loredanacirstea/wasmx-env-crosschain v0.0.0 // indirect + +require github.com/loredanacirstea/wasmx-staking v0.0.0 + +require github.com/loredanacirstea/wasmx-consensus-utils v0.0.0 + +require github.com/loredanacirstea/wasmx-blocks v0.0.0 + +require github.com/loredanacirstea/wasmx-env-multichain v0.0.0 // indirect + +require github.com/loredanacirstea/wasmx-raft-lib v0.0.0 + +require github.com/loredanacirstea/wasmx-raftp2p-lib v0.0.0 + +require ( + github.com/loredanacirstea/wasmx-env-p2p v0.0.0 + github.com/loredanacirstea/wasmx-kayros-verifier v0.0.0 +) + +require ( + cosmossdk.io/math v1.5.3 // indirect + github.com/loredanacirstea/wasmx-env-httpclient v0.0.0 // indirect + github.com/loredanacirstea/wasmx-env-utils v0.0.0 // indirect + github.com/loredanacirstea/wasmx-utils v0.0.0 // indirect +) + +replace github.com/loredanacirstea/wasmx-env v0.0.0 => ../wasmx-env + +replace github.com/loredanacirstea/wasmx-env-utils v0.0.0 => ../wasmx-env-utils + +replace github.com/loredanacirstea/wasmx-utils v0.0.0 => ../wasmx-utils + +replace github.com/loredanacirstea/wasmx-fsm v0.0.0 => ../wasmx-fsm + +replace github.com/loredanacirstea/wasmx-env-core v0.0.0 => ../wasmx-env-core + +replace github.com/loredanacirstea/wasmx-env-crosschain v0.0.0 => ../wasmx-env-crosschain + +replace github.com/loredanacirstea/wasmx-env-consensus v0.0.0 => ../wasmx-env-consensus + +replace github.com/loredanacirstea/wasmx-staking v0.0.0 => ../wasmx-staking + +replace github.com/loredanacirstea/wasmx-consensus-utils v0.0.0 => ../wasmx-consensus-utils + +replace github.com/loredanacirstea/wasmx-blocks v0.0.0 => ../wasmx-blocks + +replace github.com/loredanacirstea/wasmx-env-multichain v0.0.0 => ../wasmx-env-multichain + +replace github.com/loredanacirstea/wasmx-raft-lib v0.0.0 => ../wasmx-raft-lib + +replace github.com/loredanacirstea/wasmx-raftp2p-lib v0.0.0 => ../wasmx-raftp2p-lib + +replace github.com/loredanacirstea/wasmx-env-p2p v0.0.0 => ../wasmx-env-p2p + +replace github.com/loredanacirstea/wasmx-env-httpclient v0.0.0 => ../wasmx-env-httpclient + +replace github.com/loredanacirstea/wasmx-kayros-verifier v0.0.0 => ../wasmx-kayros-verifier diff --git a/tests/testdata/tinygo/wasmx-kayrosp2p-lib/go.sum b/tests/testdata/tinygo/wasmx-kayrosp2p-lib/go.sum new file mode 100644 index 00000000..18cf05fc --- /dev/null +++ b/tests/testdata/tinygo/wasmx-kayrosp2p-lib/go.sum @@ -0,0 +1,12 @@ +cosmossdk.io/math v1.5.3 h1:WH6tu6Z3AUCeHbeOSHg2mt9rnoiUWVWaQ2t6Gkll96U= +cosmossdk.io/math v1.5.3/go.mod h1:uqcZv7vexnhMFJF+6zh9EWdm/+Ylyln34IvPnBauPCQ= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/tests/testdata/tinygo/wasmx-kayrosp2p-lib/lib/actions.go b/tests/testdata/tinygo/wasmx-kayrosp2p-lib/lib/actions.go new file mode 100644 index 00000000..06ed13ab --- /dev/null +++ b/tests/testdata/tinygo/wasmx-kayrosp2p-lib/lib/actions.go @@ -0,0 +1,1052 @@ +package lib + +import ( + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + + consensuswrap "github.com/loredanacirstea/wasmx-env-consensus/lib" + typestnd "github.com/loredanacirstea/wasmx-env-consensus/lib" + p2p "github.com/loredanacirstea/wasmx-env-p2p/lib" + wasmx "github.com/loredanacirstea/wasmx-env/lib" + fsm "github.com/loredanacirstea/wasmx-fsm/lib" + raftlib "github.com/loredanacirstea/wasmx-raft-lib/lib" +) + +// Chat room topics (must match raftp2p for message propagation) +const ( + CHAT_ROOM_PROTOCOL = "chat_room_protocol" // Same as raftp2p for compatibility + CHAT_ROOM_MEMPOOL = CHAT_ROOM_PROTOCOL + CHAT_ROOM_NODE = "chat_room_node" // For commit messages + STORAGE_KEY_KAYROS_CONFIG = "kayros_config" + STORAGE_KEY_LAST_UUID = "kayros_last_uuid" + STORAGE_KEY_CONSENSUS_CONFIG = "consensus_config" + STORAGE_KEY_BLOCK_COMMITS = "block_commits_" // Prefix for block commit mappings + STORAGE_KEY_PENDING_TXS = "pending_txs" // Transactions needed for current block + STORAGE_KEY_KAYROS_TX_META = "kayros_tx_meta_" // Prefix for Kayros tx metadata + STORAGE_KEY_BLOCK_KAYROS = "block_kayros_" // Prefix for block Kayros metadata + + CTX_KAYROS_BASE_URL = "kayros_base_url" + CTX_KAYROS_USER_KEY = "kayros_user_key" + CTX_DATA_TYPE_ID = "data_type_id" + CTX_THRESHOLD_COMMIT = "threshold_commit" + CTX_THRESHOLD_FINALIZE = "threshold_finalize" + CTX_GENESIS_UUID = "genesis_uuid" + CTX_MAX_BLOCK_TX = "max_block_tx" +) + +// GetKayrosClient retrieves the Kayros client, initializing if needed +func GetKayrosClient() (*KayrosClient, error) { + config := KayrosConfig{ + ApiBaseUrl: sGet(CTX_KAYROS_BASE_URL), + ApiUserKey: sGet(CTX_KAYROS_USER_KEY), + } + kayrosClient := NewKayrosClient(config) + return kayrosClient, nil +} + +func BuildKayrosDataType() wasmx.HexString { + chainId := wasmx.GetChainId() + dataType := fmt.Sprintf("wasmx_tx_%s", chainId) + dataTypeId := sGet(CTX_DATA_TYPE_ID) + if dataTypeId != "" { + dataType = fmt.Sprintf("%s_%s", dataType, dataTypeId) + } + dataTypeBytes := make([]byte, 32) + copy(dataTypeBytes, []byte(dataType)) + return wasmx.HexString(hex.EncodeToString(dataTypeBytes)) +} + +func BuildKayrosBlockDataType() wasmx.HexString { + chainId := wasmx.GetChainId() + dataType := fmt.Sprintf("wasmx_b_%s", chainId) + dataTypeId := sGet(CTX_DATA_TYPE_ID) + if dataTypeId != "" { + dataType = fmt.Sprintf("%s_%s", dataType, dataTypeId) + } + dataTypeBytes := make([]byte, 32) + copy(dataTypeBytes, []byte(dataType)) + return wasmx.HexString(hex.EncodeToString(dataTypeBytes)) +} + +// GetLastKayrosUUID retrieves the last processed Kayros UUID from state +func GetLastKayrosUUID() string { + data := sGet(STORAGE_KEY_LAST_UUID) + return string(data) +} + +// SetLastKayrosUUID stores the last processed Kayros UUID in state +func SetLastKayrosUUID(uuid string) { + sSet(STORAGE_KEY_LAST_UUID, uuid) +} + +// GetConsensusConfig retrieves the consensus configuration from state +func GetConsensusConfig() (*ConsensusConfig, error) { + thresholdCommit := 51 // default + thresholdFinalize := 75 // default + + commitStr := sGet(CTX_THRESHOLD_COMMIT) + if commitStr != "" { + if val, err := fsm.ParseInt32(commitStr); err == nil { + thresholdCommit = int(val) + } + } + + finalizeStr := sGet(CTX_THRESHOLD_FINALIZE) + if finalizeStr != "" { + if val, err := fsm.ParseInt32(finalizeStr); err == nil { + thresholdFinalize = int(val) + } + } + + config := ConsensusConfig{ + ThresholdCommit: thresholdCommit, + ThresholdFinalize: thresholdFinalize, + GenesisUUID: sGet(CTX_GENESIS_UUID), + } + return &config, nil +} + +// GetMaxBlockTx retrieves the maximum number of transactions per block from context +func GetMaxBlockTx() int { + maxTxStr := sGet(CTX_MAX_BLOCK_TX) + if maxTxStr != "" { + if val, err := fsm.ParseInt32(maxTxStr); err == nil && val > 0 { + return int(val) + } + } + return 20 +} + +// GetBlockCommitMapping retrieves the commit mapping for a specific block +func GetBlockCommitMapping(blockNumber int64) (*BlockCommitMapping, error) { + key := fmt.Sprintf("%s%d", STORAGE_KEY_BLOCK_COMMITS, blockNumber) + data := wasmx.StorageLoad([]byte(key)) + if len(data) == 0 { + // Return empty mapping if not found + return &BlockCommitMapping{ + BlockNumber: blockNumber, + Commits: make(map[string]BlockCommit), + Finalized: false, + }, nil + } + var mapping BlockCommitMapping + if err := json.Unmarshal(data, &mapping); err != nil { + return nil, err + } + return &mapping, nil +} + +// SetBlockCommitMapping stores the commit mapping for a specific block +func SetBlockCommitMapping(mapping *BlockCommitMapping) error { + key := fmt.Sprintf("%s%d", STORAGE_KEY_BLOCK_COMMITS, mapping.BlockNumber) + data, err := json.Marshal(mapping) + if err != nil { + return err + } + wasmx.StorageStore([]byte(key), data) + return nil +} + +// GetPendingTxHashes retrieves the list of transaction hashes needed for the current block +func GetPendingTxHashes() ([]string, error) { + data := wasmx.StorageLoad([]byte(STORAGE_KEY_PENDING_TXS)) + if len(data) == 0 { + return []string{}, nil + } + var hashes []string + if err := json.Unmarshal(data, &hashes); err != nil { + return nil, err + } + return hashes, nil +} + +// SetPendingTxHashes stores the list of transaction hashes needed for the current block +func SetPendingTxHashes(hashes []string) error { + data, err := json.Marshal(hashes) + if err != nil { + return err + } + wasmx.StorageStore([]byte(STORAGE_KEY_PENDING_TXS), data) + return nil +} + +// GetKayrosTxMetadata retrieves Kayros metadata for a specific transaction +func GetKayrosTxMetadata(txHash string) (*KayrosTxMetadata, error) { + key := fmt.Sprintf("%s%s", STORAGE_KEY_KAYROS_TX_META, txHash) + data := wasmx.StorageLoad([]byte(key)) + if len(data) == 0 { + return nil, nil + } + var meta KayrosTxMetadata + if err := json.Unmarshal(data, &meta); err != nil { + return nil, err + } + return &meta, nil +} + +// SetKayrosTxMetadata stores Kayros metadata for a specific transaction +func SetKayrosTxMetadata(meta *KayrosTxMetadata) error { + key := fmt.Sprintf("%s%s", STORAGE_KEY_KAYROS_TX_META, meta.TxHash) + data, err := json.Marshal(meta) + if err != nil { + return err + } + wasmx.StorageStore([]byte(key), data) + return nil +} + +// GetBlockKayrosMetadata retrieves Kayros metadata for all transactions in a block +func GetBlockKayrosMetadata(blockNumber int64) (*KayrosBlockMetadata, error) { + key := fmt.Sprintf("%s%d", STORAGE_KEY_BLOCK_KAYROS, blockNumber) + data := wasmx.StorageLoad([]byte(key)) + if len(data) == 0 { + return nil, nil + } + var meta KayrosBlockMetadata + if err := json.Unmarshal(data, &meta); err != nil { + return nil, err + } + return &meta, nil +} + +// SetBlockKayrosMetadata stores Kayros metadata for all transactions in a block +func SetBlockKayrosMetadata(blockNumber int64, meta *KayrosBlockMetadata) error { + key := fmt.Sprintf("%s%d", STORAGE_KEY_BLOCK_KAYROS, blockNumber) + data, err := json.Marshal(meta) + if err != nil { + return err + } + wasmx.StorageStore([]byte(key), data) + return nil +} + +// BuildBlockKayrosMetadata collects Kayros metadata for all transactions in the pending list +func BuildBlockKayrosMetadata(blockNumber int64) (*KayrosBlockMetadata, error) { + pendingHashes, err := GetPendingTxHashes() + if err != nil { + return nil, err + } + + blockMeta := &KayrosBlockMetadata{ + Transactions: make([]KayrosTxMetadata, 0, len(pendingHashes)), + } + + for _, txHash := range pendingHashes { + txMeta, err := GetKayrosTxMetadata(txHash) + if err != nil { + LoggerError("failed to get Kayros metadata for tx", []string{"txHash", txHash, "error", err.Error()}) + continue + } + if txMeta != nil { + txMeta.BlockNumber = blockNumber + blockMeta.Transactions = append(blockMeta.Transactions, *txMeta) + + // Track first and last UUIDs + if blockMeta.FirstUUID == "" { + blockMeta.FirstUUID = txMeta.UUID + } + blockMeta.LastUUID = txMeta.UUID + } + } + + // Store the block metadata + if err := SetBlockKayrosMetadata(blockNumber, blockMeta); err != nil { + return nil, err + } + + return blockMeta, nil +} + +// RegisterWithKayros registers a transaction with Kayros via POST /api/grpc/single-hash +// This action is called when a new transaction is received +func RegisterWithKayros(params []fsm.ActionParam, event fsm.EventObject) (*KayrosRegistrationResponse, error) { + // Extract transaction from event params + txB64 := "" + for _, p := range event.Params { + if p.Key == "transaction" { + txB64 = p.Value + break + } + } + if txB64 == "" { + return nil, fmt.Errorf("no transaction found in event") + } + + // Decode transaction + txBytes, err := base64.StdEncoding.DecodeString(txB64) + if err != nil { + return nil, err + } + + // Compute transaction hash (hex encoded for Kayros) + txHashBytes := wasmx.Sha256(txBytes) + txHash := fmt.Sprintf("%x", txHashBytes) + + // Get Kayros client + client, err := GetKayrosClient() + if err != nil { + LoggerError("failed to get Kayros client", []string{"error", err.Error()}) + // Don't fail the transaction if Kayros is unavailable + return nil, nil + } + + // Register transaction with Kayros via POST /api/grpc/single-hash + resp, err := client.RegisterTransaction(BuildKayrosDataType(), wasmx.HexString(txHash)) + if err != nil { + LoggerError("failed to register transaction with Kayros", []string{"txHash", txHash, "error", err.Error()}) + // Don't fail the transaction if Kayros registration fails + return nil, nil + } + + if !resp.Success { + LoggerDebug("Kayros registration returned failure", []string{"txHash", txHash, "message", resp.Message}) + return nil, nil + } + + LoggerInfo("transaction registered with Kayros", []string{ + "txHash", txHash, + "uuid", string(resp.TimeUUID), + "kayros_hash", string(resp.ComputedHash), + }) + + return resp, nil +} + +// GetKayrosTxs fetches transactions from Kayros for block production +// This action is called periodically by validators to produce blocks +// It first checks the mempool for transactions, then requests missing ones from peers +func GetKayrosTxs(params []fsm.ActionParam, event fsm.EventObject) error { + client, err := GetKayrosClient() + if err != nil { + return err + } + + // Get the last processed Kayros UUID + lastUUID := GetLastKayrosUUID() + + // If no last UUID, use the genesis UUID from config + if lastUUID == "" { + config, err := GetConsensusConfig() + if err != nil { + LoggerError("failed to get consensus config for genesis UUID", []string{"error", err.Error()}) + return err + } + lastUUID = config.GenesisUUID + LoggerInfo("using genesis UUID", []string{"uuid", lastUUID}) + } + + // Fetch records from Kayros starting from the last UUID + // Use configured max or default + limit := GetMaxBlockTx() + records, err := client.GetRecordsFromPrev(BuildKayrosDataType(), lastUUID, limit) + if err != nil { + LoggerError("failed to fetch records from Kayros", []string{"error", err.Error(), "lastUUID", lastUUID}) + return err + } + + LoggerInfo("fetched records from Kayros", []string{ + "count", fmt.Sprintf("%d", len(records)), + "lastUUID", lastUUID, + }) + + if len(records) == 0 { + // No new transactions to process - clear pending + SetPendingTxHashes([]string{}) + return nil + } + + // Deduplicate records by transaction hash, keeping the oldest (smallest UUID) for each + // This handles cases where the same transaction was registered multiple times + deduplicatedRecords := make(map[string]KayrosRecord) + for _, record := range records { + txHash := record.DataItemHex + if existing, exists := deduplicatedRecords[txHash]; exists { + // Keep the one with smaller UUID (older timestamp) + if record.UuidHex < existing.UuidHex { + deduplicatedRecords[txHash] = record + LoggerDebug("found duplicate tx, keeping older registration", []string{ + "txHash", txHash, + "oldUUID", existing.UuidHex, + "newUUID", record.UuidHex, + }) + } + } else { + deduplicatedRecords[txHash] = record + } + } + + LoggerInfo("deduplicated Kayros records", []string{ + "original", fmt.Sprintf("%d", len(records)), + "deduplicated", fmt.Sprintf("%d", len(deduplicatedRecords)), + }) + + // Get mempool to check which transactions we already have + mp, err := raftlib.GetMempool() + if err != nil { + return err + } + + // Collect transaction hashes we need and ones we're missing + missingTxHashes := []string{} + allTxHashes := []string{} + + for txHash, record := range deduplicatedRecords { + // Convert hex hash to base64 for mempool check (mempool uses base64 format) + txHashBase64, err := HexHashToBase64(txHash) + if err != nil { + LoggerError("failed to decode tx hash from Kayros", []string{"txHash", txHash, "error", err.Error()}) + continue + } + + // Check if we have already processed this transaction before + // and skip it if yes + if mp.IsRecentlyProcessed(txHashBase64) { + LoggerDebug("transaction already processed", []string{"hash", txHash, "kayrosId", record.HashItemHex}) + continue + } + + allTxHashes = append(allTxHashes, txHash) + + // Store Kayros metadata for this transaction + txMeta := &KayrosTxMetadata{ + TxHash: txHash, + UUID: record.UuidHex, + HashItem: record.HashItemHex, + Timestamp: record.Timestamp, + PrevHash: record.PrevHashHex, + } + if err := SetKayrosTxMetadata(txMeta); err != nil { + LoggerError("failed to store Kayros tx metadata", []string{"txHash", txHash, "error", err.Error()}) + } + + // Check if we have this transaction in our mempool + if mp.IsInMempool(txHashBase64) { + LoggerDebug("transaction in mempool", []string{"txHash", txHash, "kayrosId", record.HashItemHex}) + continue + } + + // Transaction not in mempool - add to missing list + missingTxHashes = append(missingTxHashes, txHash) + LoggerDebug("transaction not in mempool, will request from peers", []string{ + "txHash", txHash, + "kayrosId", record.HashItemHex, + "uuid", record.UuidHex, + "timestamp", record.Timestamp, + }) + } + + // Store the list of all transactions needed for this block + if err := SetPendingTxHashes(allTxHashes); err != nil { + LoggerError("failed to store pending tx hashes", []string{"error", err.Error()}) + } + + // If we have missing transactions, request them from peers + if len(missingTxHashes) > 0 { + LoggerInfo("requesting missing transactions from peers", []string{"count", fmt.Sprintf("%d", len(missingTxHashes))}) + if err := RequestMissingTransactions(missingTxHashes); err != nil { + LoggerError("failed to request missing transactions", []string{"error", err.Error()}) + // Don't return error - we'll retry on next round + } + } + + // Note: LastKayrosUUID is updated in CommitBlock after successful block commit + // This ensures we don't skip transactions if block commit fails + return nil +} + +// ReceiveCommit handles receiving a commit from another validator +// In Kayros consensus, validators send block hashes to verify consensus +// Until a block is finalized, we may receive multiple commits from the same node +// so we use timestamps to overwrite previous commits if a newer one comes in +func ReceiveCommit(params []fsm.ActionParam, event fsm.EventObject) error { + // Extract commit data from event params + commitB64 := "" + sender := "" + for _, p := range event.Params { + if p.Key == "commit" { + commitB64 = p.Value + } else if p.Key == "sender" { + sender = p.Value + } + } + + if commitB64 == "" { + return fmt.Errorf("no commit found in event") + } + + // Decode commit data + commitData, err := base64.StdEncoding.DecodeString(commitB64) + if err != nil { + return err + } + + // Parse the commit + var commit BlockCommit + if err := json.Unmarshal(commitData, &commit); err != nil { + LoggerError("failed to parse commit", []string{"error", err.Error()}) + return err + } + + // Set sender from event if not in commit data + if commit.Sender == "" { + commit.Sender = sender + } + + LoggerDebug("received commit", []string{ + "sender", commit.Sender, + "blockNumber", fmt.Sprintf("%d", commit.BlockNumber), + "blockHash", commit.BlockHash, + "timestamp", fmt.Sprintf("%d", commit.Timestamp), + }) + + // Get the commit mapping for this block + mapping, err := GetBlockCommitMapping(commit.BlockNumber) + if err != nil { + LoggerError("failed to get block commit mapping", []string{"error", err.Error()}) + return err + } + + // If block is already finalized, ignore new commits + if mapping.Finalized { + LoggerDebug("block already finalized, ignoring commit", []string{"blockNumber", fmt.Sprintf("%d", commit.BlockNumber)}) + return nil + } + + // Check if we already have a commit from this sender + existingCommit, exists := mapping.Commits[commit.Sender] + if exists { + // Only overwrite if new commit has a newer timestamp + if commit.Timestamp <= existingCommit.Timestamp { + LoggerDebug("ignoring older commit from same sender", []string{ + "sender", commit.Sender, + "existingTimestamp", fmt.Sprintf("%d", existingCommit.Timestamp), + "newTimestamp", fmt.Sprintf("%d", commit.Timestamp), + }) + return nil + } + LoggerDebug("overwriting commit with newer timestamp", []string{ + "sender", commit.Sender, + "oldTimestamp", fmt.Sprintf("%d", existingCommit.Timestamp), + "newTimestamp", fmt.Sprintf("%d", commit.Timestamp), + }) + } + + // Store the commit + mapping.Commits[commit.Sender] = commit + + // Save the updated mapping + if err := SetBlockCommitMapping(mapping); err != nil { + LoggerError("failed to save block commit mapping", []string{"error", err.Error()}) + return err + } + + // Check consensus thresholds + if err := CheckConsensusThresholds(mapping); err != nil { + LoggerError("failed to check consensus thresholds", []string{"error", err.Error()}) + return err + } + + return nil +} + +// CheckConsensusThresholds checks if we have reached consensus thresholds for a block +func CheckConsensusThresholds(mapping *BlockCommitMapping) error { + config, err := GetConsensusConfig() + if err != nil { + return err + } + + // Get total validator count + nodes, err := raftlib.GetNodeIPs() + if err != nil { + return err + } + totalValidators := len(nodes) + if totalValidators == 0 { + return nil + } + + // Count commits by block hash + hashCounts := make(map[string]int) + for _, commit := range mapping.Commits { + hashCounts[commit.BlockHash]++ + } + + // Find the hash with the most votes + maxCount := 0 + maxHash := "" + for hash, count := range hashCounts { + if count > maxCount { + maxCount = count + maxHash = hash + } + } + + // Calculate percentages + commitPercentage := (len(mapping.Commits) * 100) / totalValidators + hashPercentage := (maxCount * 100) / totalValidators + + LoggerDebug("consensus status", []string{ + "blockNumber", fmt.Sprintf("%d", mapping.BlockNumber), + "totalCommits", fmt.Sprintf("%d", len(mapping.Commits)), + "totalValidators", fmt.Sprintf("%d", totalValidators), + "commitPercentage", fmt.Sprintf("%d", commitPercentage), + "maxHashCount", fmt.Sprintf("%d", maxCount), + "maxHash", maxHash, + "hashPercentage", fmt.Sprintf("%d", hashPercentage), + }) + + // Check finalization threshold (e.g., 75% agree on the same hash) + if hashPercentage >= config.ThresholdFinalize { + LoggerInfo("block finalized", []string{ + "blockNumber", fmt.Sprintf("%d", mapping.BlockNumber), + "blockHash", maxHash, + "percentage", fmt.Sprintf("%d", hashPercentage), + }) + mapping.Finalized = true + if err := SetBlockCommitMapping(mapping); err != nil { + return err + } + return nil + } + + // Check commit threshold for potential rollback (e.g., 51% have committed but disagree) + if commitPercentage >= config.ThresholdCommit && len(hashCounts) > 1 { + // We have enough commits but they disagree - check if we need to rollback + LoggerInfo("commit threshold reached but hashes disagree, checking rollback", []string{ + "blockNumber", fmt.Sprintf("%d", mapping.BlockNumber), + "uniqueHashes", fmt.Sprintf("%d", len(hashCounts)), + "commitPercentage", fmt.Sprintf("%d", commitPercentage), + "hashPercentage", fmt.Sprintf("%d", hashPercentage), + }) + + // CRITICAL: Only rollback if the leading hash has >= ThresholdCommit percentage + // Otherwise, with a 2-way split (e.g., 33% vs 33%), we'd arbitrarily pick one + if hashPercentage < config.ThresholdCommit { + LoggerInfo("no clear majority yet, waiting for more commits", []string{ + "blockNumber", fmt.Sprintf("%d", mapping.BlockNumber), + "hashPercentage", fmt.Sprintf("%d", hashPercentage), + "requiredPercentage", fmt.Sprintf("%d", config.ThresholdCommit), + }) + return nil + } + + // Get the hash of the specific block we're checking (not NextHash which is for the next block) + blockEntry, err := raftlib.GetLogEntryAggregate(mapping.BlockNumber) + if err != nil { + LoggerError("failed to get block entry for consensus check", []string{ + "blockNumber", fmt.Sprintf("%d", mapping.BlockNumber), + "error", err.Error(), + }) + return err + } + if blockEntry == nil { + // We don't have this block yet, can't compare + LoggerDebug("we don't have this block yet, skipping consensus check", []string{ + "blockNumber", fmt.Sprintf("%d", mapping.BlockNumber), + }) + return nil + } + + // Get the hash from the block header + var header typestnd.Header + if err := json.Unmarshal(blockEntry.Data.Header, &header); err != nil { + LoggerError("failed to unmarshal block header", []string{"error", err.Error()}) + return err + } + blockHash, err := consensuswrap.HeaderHash(header) + if err != nil { + LoggerError("failed to compute header hash", []string{"error", err.Error()}) + return err + } + ourHash := base64.StdEncoding.EncodeToString(blockHash) + + // If our hash doesn't match the majority hash, we need to rollback + if ourHash != maxHash { + LoggerInfo("our hash doesn't match majority, initiating rollback", []string{ + "blockNumber", fmt.Sprintf("%d", mapping.BlockNumber), + "ourHash", ourHash, + "majorityHash", maxHash, + "majorityPercentage", fmt.Sprintf("%d", hashPercentage), + }) + + // Get current height to determine how many blocks to rollback + currentHeight, err := raftlib.GetLastLogIndex() + if err != nil { + return err + } + + // Target height is the block before the disagreement + targetHeight := mapping.BlockNumber - 1 + + // Rollback one block at a time from current height down to target height + // raft.Rollback expects the height to rollback TO (not FROM) + for height := currentHeight; height > targetHeight; height-- { + LoggerDebug("rolling back block", []string{ + "height", fmt.Sprintf("%d", height), + "targetHeight", fmt.Sprintf("%d", targetHeight), + }) + + err := raftlib.Rollback([]fsm.ActionParam{ + {Key: "height", Value: fmt.Sprintf("%d", height)}, + }, fsm.EventObject{}) + if err != nil { + LoggerError("rollback failed", []string{"height", fmt.Sprintf("%d", height), "error", err.Error()}) + return err + } + } + + // Clear pending txs and reset Kayros UUID to re-fetch + SetPendingTxHashes([]string{}) + + // Reset LastKayrosUUID to the UUID from the last good block + // This will cause GetKayrosTxs to re-fetch from that point + lastGoodBlockMeta, err := GetBlockKayrosMetadata(targetHeight) + if err == nil && lastGoodBlockMeta != nil && lastGoodBlockMeta.LastUUID != "" { + SetLastKayrosUUID(lastGoodBlockMeta.LastUUID) + LoggerInfo("reset Kayros UUID after rollback", []string{"uuid", lastGoodBlockMeta.LastUUID}) + } + + LoggerInfo("rollback completed", []string{ + "fromHeight", fmt.Sprintf("%d", currentHeight), + "toHeight", fmt.Sprintf("%d", targetHeight), + }) + } + } + + return nil +} + +// VerifyCommitLight performs light verification of a commit +func VerifyCommitLight(params []fsm.ActionParam, event fsm.EventObject) error { + // TODO: Implement light client verification of commits + // This could verify signatures and basic structure without full state verification + return nil +} + +// RequestMissingTransactions sends a P2P request to peers for missing transactions +func RequestMissingTransactions(txHashes []string) error { + if len(txHashes) == 0 { + return nil + } + + // Get current block number + lastBlockIndex, err := raftlib.GetLastBlockIndex() + if err != nil { + return err + } + nextBlockNumber := lastBlockIndex + 1 + + // Get our node info + nodes, err := raftlib.GetNodeIPs() + if err != nil { + return err + } + ourId, err := raftlib.GetCurrentNodeId() + if err != nil { + return err + } + if int(ourId) >= len(nodes) { + return fmt.Errorf("our node ID out of range") + } + ourAddress := string(nodes[ourId].Address) + + // Create request + request := MissingTransactionsRequest{ + TxHashes: txHashes, + BlockNumber: nextBlockNumber, + Sender: ourAddress, + } + + reqBytes, err := json.Marshal(request) + if err != nil { + return err + } + + // Build P2P message payload + payload := struct { + Run struct { + Event struct { + Type string `json:"type"` + Params []struct { + Key string `json:"key"` + Value string `json:"value"` + } `json:"params"` + } `json:"event"` + } `json:"run"` + }{} + payload.Run.Event.Type = "receiveMissingTransactionsRequest" + payload.Run.Event.Params = []struct { + Key string `json:"key"` + Value string `json:"value"` + }{ + {Key: "request", Value: base64.StdEncoding.EncodeToString(reqBytes)}, + {Key: "sender", Value: ourAddress}, + } + msg, _ := json.Marshal(&payload) + + // Send to all peers + peers := make([]string, 0, len(nodes)-1) + for i := range nodes { + if int32(i) != ourId && raftlib.IsNodeActive(nodes[i]) { + peers = append(peers, nodes[i].Node.IP) + } + } + + if len(peers) == 0 { + LoggerDebug("no peers available to request missing transactions", nil) + return nil + } + + contract := wasmx.GetAddress() + chainId := wasmx.GetChainId() + protocolId := fmt.Sprintf("%s_%s", PROTOCOL_ID, chainId) + _, err = p2p.SendMessageToPeers(p2p.SendMessageToPeersRequest{ + Contract: contract, + Sender: contract, + Msg: msg, + ProtocolId: protocolId, + Peers: peers, + }) + + if err != nil { + LoggerError("failed to send missing transactions request", []string{"error", err.Error()}) + return err + } + + LoggerInfo("sent missing transactions request", []string{ + "txCount", fmt.Sprintf("%d", len(txHashes)), + "peerCount", fmt.Sprintf("%d", len(peers)), + }) + + return nil +} + +// ReceiveMissingTransactionsRequest handles incoming requests for missing transactions +// This is called when another node asks us for transactions it doesn't have +func ReceiveMissingTransactionsRequest(params []fsm.ActionParam, event fsm.EventObject) error { + // Extract request from event params + requestB64 := "" + sender := "" + for _, p := range event.Params { + if p.Key == "request" { + requestB64 = p.Value + } else if p.Key == "sender" { + sender = p.Value + } + } + + if requestB64 == "" { + return fmt.Errorf("no request found in event") + } + + // Decode request + requestData, err := base64.StdEncoding.DecodeString(requestB64) + if err != nil { + return err + } + + var request MissingTransactionsRequest + if err := json.Unmarshal(requestData, &request); err != nil { + LoggerError("failed to parse missing transactions request", []string{"error", err.Error()}) + return err + } + + LoggerDebug("received missing transactions request", []string{ + "sender", sender, + "txCount", fmt.Sprintf("%d", len(request.TxHashes)), + "blockNumber", fmt.Sprintf("%d", request.BlockNumber), + }) + + // Get mempool to find transactions + mp, err := raftlib.GetMempool() + if err != nil { + return err + } + + // Collect transactions we have + transactions := make([][]byte, 0) + foundHashes := make([]string, 0) + + for _, txHash := range request.TxHashes { + // Check if transaction exists in mempool map + if mempoolTx, ok := mp.Map[txHash]; ok { + transactions = append(transactions, mempoolTx.Tx) + foundHashes = append(foundHashes, txHash) + } + } + + if len(transactions) == 0 { + LoggerDebug("no requested transactions found in mempool", []string{"sender", sender}) + return nil + } + + // Get our node info + nodes, err := raftlib.GetNodeIPs() + if err != nil { + return err + } + ourId, err := raftlib.GetCurrentNodeId() + if err != nil { + return err + } + ourAddress := string(nodes[ourId].Address) + + // Create response + response := MissingTransactionsResponse{ + Transactions: transactions, + TxHashes: foundHashes, + Sender: ourAddress, + } + + respBytes, err := json.Marshal(response) + if err != nil { + return err + } + + // Build P2P message payload + payload := struct { + Run struct { + Event struct { + Type string `json:"type"` + Params []struct { + Key string `json:"key"` + Value string `json:"value"` + } `json:"params"` + } `json:"event"` + } `json:"run"` + }{} + payload.Run.Event.Type = "receiveMissingTransactions" + payload.Run.Event.Params = []struct { + Key string `json:"key"` + Value string `json:"value"` + }{ + {Key: "response", Value: base64.StdEncoding.EncodeToString(respBytes)}, + {Key: "sender", Value: ourAddress}, + } + msg, _ := json.Marshal(&payload) + + // Send response to the requesting peer + contract := wasmx.GetAddress() + chainId := wasmx.GetChainId() + protocolId := fmt.Sprintf("%s_%s", PROTOCOL_ID, chainId) + _, err = p2p.SendMessageToPeers(p2p.SendMessageToPeersRequest{ + Contract: contract, + Sender: contract, + Msg: msg, + ProtocolId: protocolId, + Peers: []string{sender}, + }) + + if err != nil { + LoggerError("failed to send missing transactions response", []string{"error", err.Error()}) + return err + } + + LoggerInfo("sent missing transactions response", []string{ + "txCount", fmt.Sprintf("%d", len(transactions)), + "to", sender, + }) + + return nil +} + +// ReceiveMissingTransactions handles incoming missing transactions from peers +// This is called when a peer responds to our request for missing transactions +func ReceiveMissingTransactions(params []fsm.ActionParam, event fsm.EventObject) error { + // Extract response from event params + responseB64 := "" + sender := "" + for _, p := range event.Params { + if p.Key == "response" { + responseB64 = p.Value + } else if p.Key == "sender" { + sender = p.Value + } + } + + if responseB64 == "" { + return fmt.Errorf("no response found in event") + } + + // Decode response + responseData, err := base64.StdEncoding.DecodeString(responseB64) + if err != nil { + return err + } + + var response MissingTransactionsResponse + if err := json.Unmarshal(responseData, &response); err != nil { + LoggerError("failed to parse missing transactions response", []string{"error", err.Error()}) + return err + } + + LoggerDebug("received missing transactions", []string{ + "sender", sender, + "txCount", fmt.Sprintf("%d", len(response.Transactions)), + }) + + addedCount := 0 + for _, tx := range response.Transactions { + raftlib.AddTransactionToMempool(tx) + } + LoggerInfo("added missing transactions to mempool", []string{ + "addedCount", fmt.Sprintf("%d", addedCount), + "fromPeer", sender, + }) + return nil +} + +// IfAllTransactions is a guard that checks if we have all transactions needed for the current block +func IfAllTransactions(params []fsm.ActionParam, event fsm.EventObject) bool { + // Get pending transaction hashes + pendingHashes, err := GetPendingTxHashes() + if err != nil { + LoggerError("failed to get pending tx hashes", []string{"error", err.Error()}) + return false + } + + // If no pending transactions, we're ready (empty block) + if len(pendingHashes) == 0 { + LoggerDebug("no pending transactions, ready for block", nil) + return true + } + + // Get mempool to check which transactions we have + mp, err := raftlib.GetMempool() + if err != nil { + LoggerError("failed to get mempool", []string{"error", err.Error()}) + return false + } + + // Check if all pending transactions are in mempool + missingCount := 0 + for _, txHash := range pendingHashes { + // Convert hex hash to base64 for mempool check (mempool uses base64 format) + txHashBase64, err := HexHashToBase64(txHash) + if err != nil { + LoggerError("failed to convert tx hash to base64", []string{"txHash", txHash, "error", err.Error()}) + missingCount++ + continue + } + + if !mp.IsInMempool(txHashBase64) { + missingCount++ + } + } + + if missingCount > 0 { + LoggerDebug("still missing transactions", []string{ + "missingCount", fmt.Sprintf("%d", missingCount), + "totalPending", fmt.Sprintf("%d", len(pendingHashes)), + }) + return false + } + + LoggerDebug("all transactions available", []string{ + "count", fmt.Sprintf("%d", len(pendingHashes)), + }) + return true +} diff --git a/tests/testdata/tinygo/wasmx-kayrosp2p-lib/lib/block.go b/tests/testdata/tinygo/wasmx-kayrosp2p-lib/lib/block.go new file mode 100644 index 00000000..d264e311 --- /dev/null +++ b/tests/testdata/tinygo/wasmx-kayrosp2p-lib/lib/block.go @@ -0,0 +1,587 @@ +package lib + +import ( + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "strings" + "time" + + blocks "github.com/loredanacirstea/wasmx-blocks/lib" + consutils "github.com/loredanacirstea/wasmx-consensus-utils/lib" + consensuswrap "github.com/loredanacirstea/wasmx-env-consensus/lib" + typestnd "github.com/loredanacirstea/wasmx-env-consensus/lib" + p2p "github.com/loredanacirstea/wasmx-env-p2p/lib" + wasmx "github.com/loredanacirstea/wasmx-env/lib" + fsm "github.com/loredanacirstea/wasmx-fsm/lib" + raftlib "github.com/loredanacirstea/wasmx-raft-lib/lib" + stakinglib "github.com/loredanacirstea/wasmx-staking/lib" +) + +const ( + METAINFO_KEY_KAYROS_RECORDS = "kayros_records" +) + +// CommitBlock builds and commits a block using Kayros-ordered transactions +// This is called as an entry action when transitioning to the propose state +func CommitBlock(params []fsm.ActionParam, event fsm.EventObject) error { + // Get pending transaction hashes (ordered by Kayros) + pendingHashes, err := GetPendingTxHashes() + if err != nil { + return fmt.Errorf("failed to get pending tx hashes: %v", err) + } + + LoggerInfo("committing block with Kayros-ordered transactions", []string{ + "txCount", fmt.Sprintf("%d", len(pendingHashes)), + }) + + // Get mempool to fetch actual transaction bytes + mp, err := raftlib.GetMempool() + if err != nil { + return fmt.Errorf("failed to get mempool: %v", err) + } + + // Collect transactions in Kayros order and track last UUID + txs := make([][]byte, 0, len(pendingHashes)) + var lastUUID string + for _, txHash := range pendingHashes { + // Convert hex hash to base64 for mempool lookup (mempool uses base64 keys) + txHashBase64, err := HexHashToBase64(txHash) + if err != nil { + LoggerError("failed to convert tx hash to base64", []string{"txHash", txHash, "error", err.Error()}) + continue + } + + if mempoolTx, ok := mp.Map[txHashBase64]; ok { + txs = append(txs, mempoolTx.Tx) + // Get the Kayros metadata for this tx to track last UUID + if txMeta, err := GetKayrosTxMetadata(txHash); err == nil && txMeta != nil { + lastUUID = txMeta.UUID + } + } else { + LoggerError("transaction not found in mempool", []string{"txHash", txHash, "txHashBase64", txHashBase64}) + // Skip missing transactions - they should have been fetched + } + } + + // Build and commit the block + // In Kayros P2P, all validators build blocks independently, so disable optimistic execution + err = buildKayrosBlockProposal(txs, pendingHashes, false, raftlib.MaxBlockSizeBytes) + if err != nil { + return err + } + + // After successful block commit, update the last Kayros UUID + if lastUUID != "" { + SetLastKayrosUUID(lastUUID) + LoggerDebug("updated last Kayros UUID after block commit", []string{"uuid", lastUUID}) + } + + // Clear pending transaction hashes for next block + if err := SetPendingTxHashes([]string{}); err != nil { + LoggerError("failed to clear pending tx hashes", []string{"error", err.Error()}) + } + + return nil +} + +// buildKayrosBlockProposal creates a block proposal with Kayros metadata +func buildKayrosBlockProposal(txs [][]byte, txHashes []string, optimisticExecution bool, maxDataBytes int64) error { + if txs == nil { + txs = make([][]byte, 0) + } + + // Get last log index for height + last, err := raftlib.GetLastLogIndex() + if err != nil { + return err + } + height := last + 1 + LoggerDebug("start Kayros block proposal", []string{"height", raftlib.Int64ToString(height)}) + + // Gather state and validators + st, err := raftlib.GetCurrentState() + if err != nil { + return err + } + validators, err := raftlib.GetAllValidators() + if err != nil { + return err + } + activeInfos, err := consutils.GetActiveValidatorInfo(validators) + if err != nil { + return err + } + validatorInfos := consutils.SortTendermintValidators(activeInfos) + validatorSet := typestnd.TendermintValidators{Validators: validatorInfos} + + // Use the first validator as proposer for deterministic block hash across all validators + // TODO we need to change Kayros to accept some additional data, like the proposerAddress + var proposerAddress wasmx.HexString + var proposerBech32 wasmx.Bech32String + if len(validatorInfos) > 0 { + proposerAddress = validatorInfos[0].HexAddress + proposerBech32 = validatorInfos[0].OperatorAddress + } else { + proposerAddress = wasmx.HexString("") + proposerBech32 = wasmx.Bech32String("") + } + + // lastBlockCommit := getLastBlockCommitInternal(st) + + // Get previous block validators for the last block commit signatures + previousBlock, err := raftlib.GetLogEntryAggregate(height - 1) + var previousValidatorSet typestnd.TendermintValidators + if previousBlock != nil { + err = json.Unmarshal(previousBlock.Data.ValidatorInfo, &previousValidatorSet) + } else { + previousValidatorSet = validatorSet + } + + // signatures := consutils.FilterAndSortCommitSignatures(lastBlockCommit.Signatures, previousValidatorSet.Validators) + // if len(signatures) != len(previousValidatorSet.Validators) && height > (raftlib.LOG_START+1) { + // raftlib.Revert(fmt.Sprintf(`last block validator set length mismatch with signature list: expected %d, got %d`, len(signatures), len(previousValidatorSet.Validators))) + // } + + lastCommit := typestnd.CommitInfo{Round: 0, Votes: []typestnd.VoteInfo{}} + localLastCommit := typestnd.ExtendedCommitInfo{Round: 0, Votes: []typestnd.ExtendedVoteInfo{}} + // for i := 0; i < len(signatures); i++ { + // commitSig := signatures[i] + // val := previousValidatorSet.Validators[i] + + // vaddress := wasmx.AddrCanonicalize(string(val.OperatorAddress)) + // validator := typestnd.Validator{Address: vaddress, Power: val.VotingPower} + // voteInfo := typestnd.VoteInfo{Validator: validator, BlockIDFlag: commitSig.BlockIDFlag} + // lastCommit.Votes = append(lastCommit.Votes, voteInfo) + + // extendedVoteInfo := typestnd.ExtendedVoteInfo{ + // Validator: validator, + // VoteExtension: []byte{}, + // ExtensionSignature: []byte{}, + // BlockIDFlag: commitSig.BlockIDFlag, + // } + // localLastCommit.Votes = append(localLastCommit.Votes, extendedVoteInfo) + // } + + nextValsHash, err := consensuswrap.ValidatorsHash(validatorInfos) + if err != nil { + return err + } + misbehavior := []typestnd.Misbehavior{} + + // STEP 1: Get block data for temporary header (without calling PrepareProposal yet) + // sortedBlockCommits := lastBlockCommit + // if height > (raftlib.LOG_START + 1) { + // sortedBlockCommits, err = consutils.GetSortedBlockCommits(lastBlockCommit, previousValidatorSet.Validators) + // sortedBlockCommits = consutils.CleanAbsentCommits(sortedBlockCommits) + // } + // empty, we dont need to keep signatures from validators + // TODO revisit this if we extend kayros to receive signatures and blob data from nodes + sortedBlockCommits := typestnd.BlockCommit{ + Height: height, + Round: height, + BlockID: st.LastBlockID, + } + evidence := typestnd.Evidence{} + consHash := []byte{} + if params, err := raftlib.GetConsensusParams(height); err == nil && params != nil { + if h, err2 := consutils.GetConsensusParamsHash(*params); err2 == nil { + consHash = h + } + } + + lastCommitHashHex := wasmx.HexString(strings.ToUpper(hex.EncodeToString(consutils.GetCommitHash(sortedBlockCommits)))) + + // STEP 2: Create temporary header with zero timestamp to calculate hash for Kayros + // Use a valid zero timestamp in RFC3339Nano format + zeroTime := "0001-01-01T00:00:00.000000000Z" + + tempHeader := typestnd.Header{ + Version: typestnd.VersionConsensus{Block: typestnd.BlockProtocol, App: st.Version.Consensus.App}, + ChainID: st.ChainID, + Height: height, + Time: zeroTime, // Valid zero timestamp for initial hash + LastBlockID: st.LastBlockID, + LastCommitHash: lastCommitHashHex, + DataHash: wasmx.HexString(strings.ToUpper(hex.EncodeToString(consutils.GetTxsHash(txs)))), + ValidatorsHash: wasmx.HexString(strings.ToUpper(hex.EncodeToString(nextValsHash))), + NextValidatorsHash: wasmx.HexString(strings.ToUpper(hex.EncodeToString(nextValsHash))), + ConsensusHash: wasmx.HexString(strings.ToUpper(hex.EncodeToString(consHash))), + AppHash: wasmx.HexString(strings.ToUpper(hex.EncodeToString(st.AppHash))), + LastResultsHash: wasmx.HexString(strings.ToUpper(hex.EncodeToString(st.LastResultsHash))), + EvidenceHash: wasmx.HexString(strings.ToUpper(hex.EncodeToString(consutils.GetEvidenceHash(evidence)))), + ProposerAddress: proposerAddress, // First validator for deterministic hash + } + + // STEP 3: Calculate hash of header with empty timestamp + tempHash, err := consensuswrap.HeaderHash(tempHeader) + if err != nil { + return err + } + tempHashHex := hex.EncodeToString(tempHash) + + // STEP 4: Get deterministic timestamp from Kayros + kayrosClient, err := GetKayrosClient() + if err != nil { + return fmt.Errorf("failed to get Kayros client: %w", err) + } + + kayrosTimestamp, err := kayrosClient.GetBlockTimestamp(BuildKayrosBlockDataType(), wasmx.HexString(tempHashHex)) + if err != nil { + return fmt.Errorf("failed to get block timestamp from Kayros: %w", err) + } + + LoggerInfo("got Kayros timestamp for block", []string{ + "height", raftlib.Int64ToString(height), + "tempHash", tempHashHex, + "kayrosTimestamp", kayrosTimestamp, + }) + + // STEP 5: Now call PrepareProposal with the Kayros timestamp + prepareReq := typestnd.RequestPrepareProposal{ + MaxTxBytes: maxDataBytes, + Txs: txs, + LocalLastCommit: localLastCommit, + Misbehavior: misbehavior, + Height: height, + Time: kayrosTimestamp, // Use Kayros timestamp + NextValidatorsHash: nextValsHash, + ProposerAddress: proposerAddress, // First validator for deterministic hash + } + prepareResp, err := consensuswrap.PrepareProposal(prepareReq) + if err != nil { + return err + } + + // STEP 6: Build final header with Kayros timestamp + header := typestnd.Header{ + Version: typestnd.VersionConsensus{Block: typestnd.BlockProtocol, App: st.Version.Consensus.App}, + ChainID: st.ChainID, + Height: height, + Time: kayrosTimestamp, // Use Kayros timestamp + LastBlockID: st.LastBlockID, + LastCommitHash: lastCommitHashHex, + DataHash: wasmx.HexString(strings.ToUpper(hex.EncodeToString(consutils.GetTxsHash(prepareResp.Txs)))), + ValidatorsHash: wasmx.HexString(strings.ToUpper(hex.EncodeToString(nextValsHash))), + NextValidatorsHash: wasmx.HexString(strings.ToUpper(hex.EncodeToString(nextValsHash))), + ConsensusHash: wasmx.HexString(strings.ToUpper(hex.EncodeToString(consHash))), + AppHash: wasmx.HexString(strings.ToUpper(hex.EncodeToString(st.AppHash))), + LastResultsHash: wasmx.HexString(strings.ToUpper(hex.EncodeToString(st.LastResultsHash))), + EvidenceHash: wasmx.HexString(strings.ToUpper(hex.EncodeToString(consutils.GetEvidenceHash(evidence)))), + ProposerAddress: proposerAddress, // First validator for deterministic hash + } + + // STEP 7: Calculate final hash with Kayros timestamp + hhash, err := consensuswrap.HeaderHash(header) + if err != nil { + return err + } + + LoggerInfo("Kayros block proposal with deterministic timestamp", []string{ + "height", raftlib.Int64ToString(height), + "hash", base64.StdEncoding.EncodeToString(hhash), + "timestamp", kayrosTimestamp, + "txCount", fmt.Sprintf("%d", len(txs)), + }) + + processReq := typestnd.RequestProcessProposal{ + Txs: prepareResp.Txs, + ProposedLastCommit: lastCommit, + Misbehavior: prepareReq.Misbehavior, + Hash: hhash, + Height: prepareReq.Height, + Time: kayrosTimestamp, // Use Kayros timestamp + NextValidatorsHash: prepareReq.NextValidatorsHash, + ProposerAddress: proposerAddress, // First validator for deterministic hash + } + processResp, err := consensuswrap.ProcessProposal(processReq) + if err != nil { + return err + } + if processResp.Status == typestnd.ProposalStatus_REJECT { + LoggerError("Kayros block rejected", []string{"height", raftlib.Int64ToString(processReq.Height)}) + return nil + } + + // Build metainfo with Kayros records + metainfo := map[string][]byte{} + if optimisticExecution { + if oe, err := consensuswrap.OptimisticExecution(processReq, processResp); err == nil { + metainfo = oe.Metainfo + } + } + + // Add Kayros metadata to metainfo + kayrosBlockMeta, err := BuildBlockKayrosMetadata(height) + if err != nil { + LoggerError("failed to build Kayros block metadata", []string{"error", err.Error()}) + } else if kayrosBlockMeta != nil && len(kayrosBlockMeta.Transactions) > 0 { + // kayrosMetaBytes, err := json.Marshal(kayrosBlockMeta) + // if err != nil { + // LoggerError("failed to marshal Kayros metadata", []string{"error", err.Error()}) + // } else { + // metainfo[METAINFO_KEY_KAYROS_RECORDS] = kayrosMetaBytes + // LoggerInfo("added Kayros metadata to block", []string{ + // "txCount", fmt.Sprintf("%d", len(kayrosBlockMeta.Transactions)), + // "firstUUID", kayrosBlockMeta.FirstUUID, + // "lastUUID", kayrosBlockMeta.LastUUID, + // }) + // } + } + + st, _ = raftlib.GetCurrentState() + st.NextHash = hhash + raftlib.SetCurrentState(st) + + err = appendLogWithKayrosMeta(processReq, header, sortedBlockCommits, optimisticExecution, metainfo, validatorSet, proposerBech32) + if err != nil { + return err + } + + // In Kayros P2P, finalize the block immediately after appending to log + // (no voting phase - each validator finalizes independently) + entryobj, err := raftlib.GetLogEntryAggregate(height) + if err != nil { + return fmt.Errorf("failed to get log entry for finalization: %v", err) + } + if entryobj != nil { + _, err := raftlib.StartBlockFinalizationInternal(entryobj, false) + if err != nil { + return fmt.Errorf("failed to finalize block: %v", err) + } + // Update last applied to mark this block as finalized + if err := raftlib.SetLastApplied(height); err != nil { + return fmt.Errorf("failed to set last applied: %v", err) + } + } + + return nil +} + +// appendLogWithKayrosMeta creates a BlockEntry and LogEntryAggregate with Kayros metadata +func appendLogWithKayrosMeta(processReq typestnd.RequestProcessProposal, header typestnd.Header, blockCommit typestnd.BlockCommit, optimisticExecution bool, meta map[string][]byte, validatorSet typestnd.TendermintValidators, proposerBech32 wasmx.Bech32String) error { + if meta == nil { + meta = make(map[string][]byte) + } + + // Create RequestProcessProposalWithMetaInfo + wrap := typestnd.RequestProcessProposalWithMetaInfo{ + Request: processReq, + OptimisticExecution: optimisticExecution, + Metainfo: meta, + } + + blockData, err := json.Marshal(&wrap) + if err != nil { + return fmt.Errorf("failed to marshal block data: %v", err) + } + + blockHeader, err := json.Marshal(&header) + if err != nil { + return fmt.Errorf("failed to marshal block header: %v", err) + } + + commit, err := json.Marshal(&blockCommit) + if err != nil { + return fmt.Errorf("failed to marshal block commit: %v", err) + } + + leaderId, err := raftlib.GetCurrentNodeId() + if err != nil { + return fmt.Errorf("failed to get current node ID: %v", err) + } + + contractAddress := wasmx.GetAddressBz() + + validatorSetBytes, err := json.Marshal(&validatorSet) + if err != nil { + return fmt.Errorf("failed to marshal validator set: %v", err) + } + + // Create BlockEntry with deterministic proposer address to ensure same block hash + blockEntry := blocks.BlockEntry{ + Index: processReq.Height, + ReaderContract: contractAddress, + WriterContract: contractAddress, + Data: blockData, + Header: blockHeader, + ProposerAddress: proposerBech32, // First validator for deterministic hash + LastCommit: commit, + Evidence: []byte(`{"evidence":[]}`), + Result: []byte{}, + ValidatorInfo: validatorSetBytes, + } + + // Create LogEntryAggregate + entry := raftlib.LogEntryAggregate{ + Index: processReq.Height, + TermID: 0, + LeaderID: leaderId, + Data: blockEntry, + } + + return raftlib.AppendLogEntry(entry) +} + +// SendCommit broadcasts a commit message to other validators +func SendCommit(params []fsm.ActionParam, event fsm.EventObject) error { + st, err := raftlib.GetCurrentState() + if err != nil { + return err + } + + lastIndex, err := raftlib.GetLastLogIndex() + if err != nil { + return err + } + + // Get current node info + nodes, err := raftlib.GetNodeIPs() + if err != nil { + return err + } + ourId, err := raftlib.GetCurrentNodeId() + if err != nil { + return err + } + if int(ourId) >= len(nodes) { + return fmt.Errorf("our node ID out of range") + } + ourAddress := string(nodes[ourId].Address) + + // Create commit message + commit := BlockCommit{ + BlockNumber: lastIndex, + BlockHash: base64.StdEncoding.EncodeToString(st.NextHash), + Sender: ourAddress, + Timestamp: time.Now().UnixMilli(), + } + + // Sign the commit + commitBytes, err := json.Marshal(commit) + if err != nil { + return err + } + signature, err := raftlib.SignMessage(string(commitBytes)) + if err != nil { + LoggerError("failed to sign commit", []string{"error", err.Error()}) + } + commit.Signature = signature + + commitBytes, _ = json.Marshal(commit) + + // Store our own commit in the mapping before broadcasting + mapping, err := GetBlockCommitMapping(lastIndex) + if err != nil { + LoggerError("failed to get block commit mapping", []string{"error", err.Error()}) + } else { + mapping.Commits[ourAddress] = commit + if err := SetBlockCommitMapping(mapping); err != nil { + LoggerError("failed to save our own commit", []string{"error", err.Error()}) + } else { + LoggerDebug("stored our own commit", []string{ + "blockNumber", fmt.Sprintf("%d", lastIndex), + "blockHash", commit.BlockHash, + }) + } + } + + // Build P2P message payload + payload := struct { + Run struct { + Event struct { + Type string `json:"type"` + Params []struct { + Key string `json:"key"` + Value string `json:"value"` + } `json:"params"` + } `json:"event"` + } `json:"run"` + }{} + payload.Run.Event.Type = "receiveCommit" + payload.Run.Event.Params = []struct { + Key string `json:"key"` + Value string `json:"value"` + }{ + {Key: "commit", Value: base64.StdEncoding.EncodeToString(commitBytes)}, + {Key: "sender", Value: ourAddress}, + } + msg, _ := json.Marshal(&payload) + + // Broadcast to chat room instead of sending to individual peers + contract := wasmx.GetAddress() + chainId := wasmx.GetChainId() + protocolId := fmt.Sprintf("%s_%s", PROTOCOL_ID, chainId) + topic := fmt.Sprintf("%s_%s", CHAT_ROOM_PROTOCOL, chainId) + + _, err = p2p.SendMessageToChatRoom(p2p.SendMessageToChatRoomRequest{ + Contract: contract, + Sender: contract, + Msg: msg, + ProtocolId: protocolId, + Topic: topic, + }) + + if err != nil { + LoggerError("failed to send commit to chat room", []string{"error", err.Error(), "topic", topic}) + return err + } + + LoggerInfo("sent commit to chat room", []string{ + "blockNumber", fmt.Sprintf("%d", commit.BlockNumber), + "blockHash", commit.BlockHash, + "topic", topic, + }) + + return nil +} + +// Helper functions copied/adapted from raft + +// getLastBlockCommitInternal gets the last block commit from state +func getLastBlockCommitInternal(st raftlib.CurrentState) typestnd.BlockCommit { + return typestnd.BlockCommit{ + Height: st.NextHeight - 1, + Round: 0, + BlockID: st.LastBlockID, + Signatures: st.LastBlockSigs, + } +} + +// getValidatorByHexAddrInternal queries the staking module for a validator +func getValidatorByHexAddrInternal(addr wasmx.HexString) (stakinglib.Validator, error) { + payload := map[string]any{"ValidatorByHexAddr": map[string]any{"validator_addr": string(addr)}} + bz, err := json.Marshal(&payload) + if err != nil { + return stakinglib.Validator{}, err + } + resp, err := callStakingInternal(string(bz), true) + if err != nil { + return stakinglib.Validator{}, err + } + if resp.Success > 0 { + return stakinglib.Validator{}, fmt.Errorf("validator not found: %s", addr) + } + if resp.Data == "" { + return stakinglib.Validator{}, fmt.Errorf("validator not found: %s", addr) + } + var result struct { + Validator stakinglib.Validator `json:"validator"` + } + if err := json.Unmarshal([]byte(resp.Data), &result); err != nil { + return stakinglib.Validator{}, err + } + return result.Validator, nil +} + +// callStakingInternal calls the staking contract +func callStakingInternal(calldata string, isQuery bool) (wasmx.CallResponse, error) { + addr := wasmx.GetAddressByRole("staking") + ok, data := wasmx.CallSimple(addr, []byte(calldata), isQuery, MODULE_NAME) + resp := wasmx.CallResponse{Success: 0, Data: string(data)} + if !ok { + resp.Success = 1 + } + return resp, nil +} diff --git a/tests/testdata/tinygo/wasmx-kayrosp2p-lib/lib/kayros_alias.go b/tests/testdata/tinygo/wasmx-kayrosp2p-lib/lib/kayros_alias.go new file mode 100644 index 00000000..6ce70f03 --- /dev/null +++ b/tests/testdata/tinygo/wasmx-kayrosp2p-lib/lib/kayros_alias.go @@ -0,0 +1,18 @@ +package lib + +import verifier "github.com/loredanacirstea/wasmx-kayros-verifier/lib" + +type KayrosClient = verifier.KayrosClient +type KayrosConfig = verifier.KayrosConfig +type KayrosRecord = verifier.KayrosRecord +type KayrosApiResponse = verifier.KayrosApiResponse +type KayrosRecordResponse = verifier.KayrosRecordResponse +type KayrosRecordsData = verifier.KayrosRecordsData +type KayrosRecordsResponse = verifier.KayrosRecordsResponse +type KayrosRegistrationRequest = verifier.KayrosRegistrationRequest +type KayrosRegistrationResponse = verifier.KayrosRegistrationResponse +type KayrosRegistrationResponseWrap = verifier.KayrosRegistrationResponseWrap + +func NewKayrosClient(config KayrosConfig) *KayrosClient { + return verifier.NewKayrosClient(config) +} diff --git a/tests/testdata/tinygo/wasmx-kayrosp2p-lib/lib/storage.go b/tests/testdata/tinygo/wasmx-kayrosp2p-lib/lib/storage.go new file mode 100644 index 00000000..2f921ba1 --- /dev/null +++ b/tests/testdata/tinygo/wasmx-kayrosp2p-lib/lib/storage.go @@ -0,0 +1,8 @@ +package lib + +import ( + fsm "github.com/loredanacirstea/wasmx-fsm/lib" +) + +func sGet(key string) string { return string(fsm.GetContextValueInternal(key)) } +func sSet(key string, val string) { fsm.SetContextValue(key, val) } diff --git a/tests/testdata/tinygo/wasmx-kayrosp2p-lib/lib/types.go b/tests/testdata/tinygo/wasmx-kayrosp2p-lib/lib/types.go new file mode 100644 index 00000000..ffe29fd9 --- /dev/null +++ b/tests/testdata/tinygo/wasmx-kayrosp2p-lib/lib/types.go @@ -0,0 +1,93 @@ +package lib + +import ( + raftlib "github.com/loredanacirstea/wasmx-raft-lib/lib" +) + +// Module identification and protocol constants +const ( + MODULE_NAME = "kayrosp2p" + PROTOCOL_ID = "kayrosp2p_1" +) + +// StateSyncRequest mirrors the AS request with start index +type StateSyncRequest struct { + StartIndex int64 `json:"start_index"` + PeerAddress string `json:"peer_address"` +} + +// StateSyncResponse mirrors the AS response with batch indexes and entries +type StateSyncResponse struct { + StartBatchIndex int64 `json:"start_batch_index"` + LastBatchIndex int64 `json:"last_batch_index"` + LastLogIndex int64 `json:"last_log_index"` + TrustedLogIndex int64 `json:"trusted_log_index"` + TrustedLogHash []byte `json:"trusted_log_hash"` + TermID int32 `json:"termId"` + PeerAddress string `json:"peer_address"` + Entries []raftlib.LogEntryAggregate `json:"entries"` +} + +// InstantiateMsg is the message passed during contract instantiation +type InstantiateMsg struct { + KayrosApiUrl string `json:"kayros_api_url"` + GenesisUUID string `json:"genesis_uuid"` // First UUID to start from on genesis + ThresholdCommit int `json:"threshold_commit"` // Percentage threshold for commit (e.g., 51) - determines when to rollback + ThresholdFinalize int `json:"threshold_finalize"` // Percentage threshold for finalization (e.g., 75) - determines when block is finalized + MaxBlockTx int `json:"max_block_tx"` // Maximum transactions per block (default: 100) +} + +// BlockCommit represents a commit from a validator for a specific block +type BlockCommit struct { + BlockNumber int64 `json:"block_number"` + BlockHash string `json:"block_hash"` + Sender string `json:"sender"` + Timestamp int64 `json:"timestamp"` // Timestamp from the sending node + Signature string `json:"signature"` +} + +// BlockCommitMapping stores all commits for a specific block from various validators +type BlockCommitMapping struct { + BlockNumber int64 `json:"block_number"` + Commits map[string]BlockCommit `json:"commits"` // Map from sender to their commit (allows overwriting with newer timestamp) + Finalized bool `json:"finalized"` +} + +// ConsensusConfig holds the consensus thresholds +type ConsensusConfig struct { + ThresholdCommit int `json:"threshold_commit"` // Percentage for commit threshold (e.g., 51) + ThresholdFinalize int `json:"threshold_finalize"` // Percentage for finalization threshold (e.g., 75) + GenesisUUID string `json:"genesis_uuid"` // First UUID on genesis +} + +// MissingTransactionsRequest is sent to peers to request missing transactions +type MissingTransactionsRequest struct { + TxHashes []string `json:"tx_hashes"` // List of transaction hashes we need + BlockNumber int64 `json:"block_number"` // The block number we're building + Sender string `json:"sender"` // The peer requesting +} + +// MissingTransactionsResponse contains the requested transactions +type MissingTransactionsResponse struct { + Transactions [][]byte `json:"transactions"` // Raw transaction bytes + TxHashes []string `json:"tx_hashes"` // Corresponding hashes + Sender string `json:"sender"` // The peer responding +} + +// KayrosTxMetadata stores Kayros ordering information for a transaction +// This metadata is included in the block's Metainfo field +type KayrosTxMetadata struct { + TxHash string `json:"tx_hash"` // Transaction hash (data_item) + UUID string `json:"uuid"` // Kayros UUID for ordering + HashItem string `json:"hash_item"` // Kayros unique ID (hash_item_hex) + Timestamp string `json:"timestamp"` // Kayros timestamp + PrevHash string `json:"prev_hash"` // Previous hash in the ordering chain + BlockNumber int64 `json:"block_number"` // Block this tx is included in +} + +// KayrosBlockMetadata contains all Kayros metadata for transactions in a block +type KayrosBlockMetadata struct { + Transactions []KayrosTxMetadata `json:"transactions"` + FirstUUID string `json:"first_uuid"` // First UUID in this block + LastUUID string `json:"last_uuid"` // Last UUID in this block +} diff --git a/tests/testdata/tinygo/wasmx-kayrosp2p-lib/lib/utils.go b/tests/testdata/tinygo/wasmx-kayrosp2p-lib/lib/utils.go new file mode 100644 index 00000000..5afed66d --- /dev/null +++ b/tests/testdata/tinygo/wasmx-kayrosp2p-lib/lib/utils.go @@ -0,0 +1,38 @@ +package lib + +import ( + "encoding/base64" + "encoding/hex" + + wasmx "github.com/loredanacirstea/wasmx-env/lib" +) + +func LoggerInfo(msg string, parts []string) { + wasmx.LoggerInfo(MODULE_NAME, msg, parts) +} + +func LoggerError(msg string, parts []string) { + wasmx.LoggerError(MODULE_NAME, msg, parts) +} + +func LoggerDebug(msg string, parts []string) { + wasmx.LoggerDebug(MODULE_NAME, msg, parts) +} + +func LoggerDebugExtended(msg string, parts []string) { + wasmx.LoggerDebugExtended(MODULE_NAME, msg, parts) +} + +func Revert(message string) { + wasmx.RevertWithModule(MODULE_NAME, message) +} + +// HexHashToBase64 converts a hex-encoded hash to base64 format +// This is needed because mempool stores hashes in base64, but Kayros uses hex +func HexHashToBase64(hexHash string) (string, error) { + hashBytes, err := hex.DecodeString(hexHash) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(hashBytes), nil +} diff --git a/tests/testdata/tinygo/wasmx-kayrosp2p-ondemand-lib/README.md b/tests/testdata/tinygo/wasmx-kayrosp2p-ondemand-lib/README.md new file mode 100644 index 00000000..94114b5b --- /dev/null +++ b/tests/testdata/tinygo/wasmx-kayrosp2p-ondemand-lib/README.md @@ -0,0 +1,24 @@ +# Kayros Consensus + +A new type of consensus based on our Kayros indexer. Each node will be initialized with a Kayros gateway url. The Kayros indexer is the one who orders transactions and gives them a timestamp. So, the node/validator sends the user transaction to Kayros and gets a Kayros record that is should also include in the block. And will use this Kayros order to order the transactions in a block. + +Nodes therefore produce blocks continuously (they finalized blocks continuously). There is no Leader, because Kayros is the only source of truth. We will only check that nodes are in sync by propagating the block hashes and then comparing them per block. If a node sees > x% (e.g. 50%) of nodes having a different block hash, it will revert its blocks and take them from the validators with the majority. And then continue the protocol. + +## Protocol + +User sends the transaction to the node. The node adds the transaction to the mempool, registers it with Kayros if it was not registered. And forwards it to other nodes. + +Block production: +- get max `x` records with data_type `wasmx_` since the previous record hash +- match records with mempool transactions +- if a record does not have a transaction, ask the other validators for the missing txs +- if tx does not appear in x time, produce block without it +- after block is produced, the block hash and txhash list is sent to the other nodes + +Block check: +- block hashes are coming in our KAYROS_BLOCKHASH_CHATROOM and we keep them in a mapping block_number => []BlockHash , with hash and validator address; when we have > 50% we check if the hash matches our hash, if not, we rollback the blocks until that block and ask the validators with correct hash for the block. +- only after this check do we consider the block as stable (cannot be rolled back), even though the probability of producing a bad block is low (only when txs don't arrive in the mempool due to a bad actor) + +## agents info + +FinalizeBlock is part of the process to commit a block. we run this as part of block production. so we run this automatically, for every block, regardless of other nodes/validators. but in parallel, we take messages from the other nodes/validators who send their block hashes and check if those block hashes match ours; this is where our thresholds come in. if we see other validators sending a different blockhash (the message needs to be signed by the validator and we need to verify the signature), then if > CTX_THRESHOLD_COMMIT, we will rollback all the blocks until the offending block (including it) and ask validators with the correct hash for the blocks; these receiveCommit messages, with block hashes keep comming, so when we get a > CTX_THRESHOLD_FINALIZE it just means that the block for sure will not rollback, so we will no longer be concerned with receiveCommit messages about that block; but FinalizeBlock has been already called when the block was produced in the first place. it is different from RAFT, because RAFT and tendermint have an entire voting process and usually split block preparation, dissemination and finalization into multiple phases. but here, we fully produce and finalize the block locally, without voting. and we correct any issues post-block finalization, because they should be very rare. it is fast consensus, because ordering is done by kayros. diff --git a/tests/testdata/tinygo/wasmx-kayrosp2p-ondemand-lib/cmd/main.go b/tests/testdata/tinygo/wasmx-kayrosp2p-ondemand-lib/cmd/main.go new file mode 100644 index 00000000..09075be2 --- /dev/null +++ b/tests/testdata/tinygo/wasmx-kayrosp2p-ondemand-lib/cmd/main.go @@ -0,0 +1,294 @@ +package main + +import ( + "encoding/json" + + wasmx "github.com/loredanacirstea/wasmx-env/lib" + fsm "github.com/loredanacirstea/wasmx-fsm/lib" + kayrosp2p "github.com/loredanacirstea/wasmx-kayrosp2p-lib/lib" + lib "github.com/loredanacirstea/wasmx-kayrosp2p-ondemand-lib/lib" + raft "github.com/loredanacirstea/wasmx-raft-lib/lib" + raftp2p "github.com/loredanacirstea/wasmx-raftp2p-lib/lib" +) + +//go:wasm-module wasmx +//export memory_ptrlen_i64_1 +func Memory_ptrlen_i64_1() {} + +//go:wasm-module wasmx +//export wasmx_env_i64_2 +func Wasmx_env_i64_2() {} + +//go:wasm-module consensus +//export wasmx_consensus_json_i64_1 +func Wasmx_consensus_json_i64_1() {} + +//go:wasm-module wasmxcore +//export wasmx_env_core_i64_1 +func Wasmx_env_core_i64_1() {} + +//go:wasm-module crosschain +//export wasmx_crosschain_json_i64_1 +func Wasmx_crosschain_json_i64_1() {} + +//go:wasm-module multichain +//export wasmx_multichain_json_i64_1 +func Wasmx_multichain_json_i64_1() {} + +//go:wasm-module p2p +//export wasmx_p2p_json_i64_1 +func Wasmx_p2p_json_i64_1() {} + +//go:wasm-module httpclient +//export wasmx_httpclient_i64_1 +func Wasmx_httpclient_i64_1() {} + +//go:wasm-module wasmxcore +//export wasmx_nondeterministic_1 +func Wasmx_nondeterministic_1() {} + +//go:wasm-module wasmx-raftp2p +//export instantiate +func Instantiate() {} + +// note, we cannot instantiate with storage this library +// because it will read from the main consensus contract + +func main() { + // Only internal + wasmx.OnlyInternal(raftp2p.MODULE_NAME, "") + + databz := wasmx.GetCallData() + var calld fsm.ExternalActionCallData + if err := json.Unmarshal(databz, &calld); err != nil { + raftp2p.Revert("invalid call data: " + err.Error() + ": " + string(databz)) + return + } + + // Helper to read params from event + get := func(key string) string { + for _, p := range calld.Event.Params { + if p.Key == key { + return p.Value + } + } + for _, p := range calld.Params { + if p.Key == key { + return p.Value + } + } + return "" + } + + // Route methods + switch calld.Method { + case "ifNodeIsValidator": + ok, err := raftp2p.IfNodeIsValidator(calld.Params, calld.Event) + if err != nil { + raftp2p.Revert(err.Error()) + return + } + res := raft.WrapGuard(ok) + wasmx.Finish(res) + return + case "ifNewTransaction": + ok, err := raftp2p.IfNewTransaction(calld.Params, calld.Event) + if err != nil { + raftp2p.Revert(err.Error()) + return + } + res := raft.WrapGuard(ok) + wasmx.Finish(res) + return + case "ifMempoolNotEmpty": + ok, err := lib.IfMempoolNotEmpty(calld.Params, calld.Event) + if err != nil { + raftp2p.Revert(err.Error()) + return + } + res := raft.WrapGuard(ok) + wasmx.Finish(res) + return + case "ifOldTransaction": + ok, err := lib.IfOldTransaction(calld.Params, calld.Event) + if err != nil { + raftp2p.Revert(err.Error()) + return + } + res := raft.WrapGuard(ok) + wasmx.Finish(res) + return + case "ifMempoolEmpty": + ok, err := lib.IfMempoolEmpty(calld.Params, calld.Event) + if err != nil { + raftp2p.Revert(err.Error()) + return + } + res := raft.WrapGuard(ok) + wasmx.Finish(res) + return + case "ifMempoolFull": + ok, err := lib.IfMempoolFull(calld.Params, calld.Event) + if err != nil { + raftp2p.Revert(err.Error()) + return + } + res := raft.WrapGuard(ok) + wasmx.Finish(res) + return + case "setupNode": + if err := raftp2p.SetupNode(calld.Params, calld.Event); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "connectPeers": + st, err := raft.GetCurrentState() + if err != nil { + raftp2p.Revert(err.Error()) + return + } + if err := raftp2p.ConnectPeersInternal(raftp2p.GetProtocolIdFromState(st)); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "connectRooms": + if err := raftp2p.ConnectRooms(); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "registerWithKayros": + _, err := kayrosp2p.RegisterWithKayros(calld.Params, calld.Event) + if err != nil { + raftp2p.Revert(err.Error()) + return + } + case "getKayrosTxs": + err := kayrosp2p.GetKayrosTxs(calld.Params, calld.Event) + if err != nil { + raftp2p.Revert(err.Error()) + return + } + case "forwardMsgToChat": + if err := raftp2p.ForwardMsgToChat(calld.Params, calld.Event); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "requestBlockSync": + if err := raftp2p.RequestBlockSync(); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "receiveStateSyncRequest": + entry := get("entry") + sig := get("signature") + sender := get("sender") + if err := raftp2p.ReceiveStateSyncRequest(entry, sig, wasmx.Bech32String(sender)); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "receiveStateSyncResponse": + entry := get("entry") + sender := get("sender") + if err := raftp2p.ReceiveStateSyncResponse(entry, sender); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "receiveUpdateNodeResponse": + entry := get("entry") + sig := get("signature") + sender := get("sender") + if err := raftp2p.ReceiveUpdateNodeResponse(entry, sig, wasmx.Bech32String(sender)); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "receiveCommit": + if err := kayrosp2p.ReceiveCommit(calld.Params, calld.Event); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "receiveMissingTransactions": + if err := kayrosp2p.ReceiveMissingTransactions(calld.Params, calld.Event); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "receiveMissingTransactionsRequest": + if err := kayrosp2p.ReceiveMissingTransactionsRequest(calld.Params, calld.Event); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "ifAllTransactions": + ok := kayrosp2p.IfAllTransactions(nil, calld.Event) + res := raft.WrapGuard(ok) + wasmx.Finish(res) + return + case "sendNewTransactionResponse": + if err := raft.SendNewTransactionResponse(nil, fsm.EventObject{}); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "addToMempool": + if err := raftp2p.AddToMempool(calld.Params, calld.Event); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "updateNodeAndReturn": + if err := raftp2p.UpdateNodeAndReturn(calld.Params, calld.Event); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "registerValidatorWithNetwork": + if err := raftp2p.RegisterValidatorWithNetwork(nil, fsm.EventObject{}); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "setup": + if err := raft.Setup(calld.Params, calld.Event); err != nil { + raftp2p.Revert(err.Error()) + return + } + case "bootstrapAfterStateSync": + if err := raft.BootstrapAfterStateSync(calld.Params, calld.Event); err != nil { + raftp2p.Revert("bootstrapAfterStateSync failed: " + err.Error()) + return + } + case "commitAfterStateSync": + if err := raft.CommitAfterStateSync(calld.Params, calld.Event); err != nil { + raftp2p.Revert("commitAfterStateSync failed: " + err.Error()) + return + } + case "VerifyCommitLight": + if err := kayrosp2p.VerifyCommitLight(calld.Params, calld.Event); err != nil { + raftp2p.Revert("VerifyCommitLight failed: " + err.Error()) + return + } + case "rollback": + if err := raft.Rollback(calld.Params, calld.Event); err != nil { + raftp2p.Revert("Rollback failed: " + err.Error()) + return + } + case "commitBlock": + if err := kayrosp2p.CommitBlock(calld.Params, calld.Event); err != nil { + raftp2p.Revert("commitBlock failed: " + err.Error()) + return + } + case "sendCommit": + if err := kayrosp2p.SendCommit(calld.Params, calld.Event); err != nil { + raftp2p.Revert("sendCommit failed: " + err.Error()) + return + } + case "cancelActiveIntervals": + // Cancel active intervals - this is handled by the FSM runtime + // Just acknowledge the action + kayrosp2p.LoggerDebug("cancelActiveIntervals called", nil) + case "requestValidatorNodeInfoIfSynced": + // Request validator node info after state sync - stub for now + kayrosp2p.LoggerDebug("requestValidatorNodeInfoIfSynced called", nil) + case "receiveUpdateNodeRequest": + raftp2p.Revert("receiveUpdateNodeRequest not implemented") + return + default: + wasmx.Revert(append([]byte("invalid function call data: "), databz...)) + return + } + wasmx.Finish(wasmx.GetFinishData()) +} diff --git a/tests/testdata/tinygo/wasmx-kayrosp2p-ondemand-lib/diagram/def.json b/tests/testdata/tinygo/wasmx-kayrosp2p-ondemand-lib/diagram/def.json new file mode 100644 index 00000000..f0357a14 --- /dev/null +++ b/tests/testdata/tinygo/wasmx-kayrosp2p-ondemand-lib/diagram/def.json @@ -0,0 +1 @@ +{"context":[{"key":"blockTimeout","value":"timeoutCommit"},{"key":"data_type_id","value":""},{"key":"genesis_uuid","value":""},{"key":"max_tx_bytes","value":65536},{"key":"max_block_gas","value":"20000000"},{"key":"timeoutCommit","value":4000},{"key":"kayros_base_url","value":""},{"key":"kayros_user_key","value":""},{"key":"threshold_commit","value":51},{"key":"timeoutMissingTxs","value":4000},{"key":"threshold_finalize","value":75},{"key":"batchTimeout","value":1000}],"id":"Kayros-P2P-OnDemand-1","initial":"uninitialized","states":[{"name":"uninitialized","after":[],"always":[],"on":[{"name":"initialize","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized","guard":null,"actions":[],"meta":[]}]}],"entry":[],"exit":[],"initial":"","states":[]},{"name":"initialized","after":[],"always":[],"on":[],"entry":[],"exit":[],"initial":"unstarted","states":[{"name":"unstarted","after":[],"always":[],"on":[{"name":"setupNode","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.unstarted","guard":null,"actions":[{"type":"setupNode","params":[]}],"meta":[]}]},{"name":"prestart","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.prestart","guard":null,"actions":[],"meta":[]}]},{"name":"setup","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.unstarted","guard":null,"actions":[{"type":"setup","params":[]}],"meta":[]}]},{"name":"start","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.started","guard":null,"actions":[{"type":"connectPeers","params":[]},{"type":"connectRooms","params":[]},{"type":"requestBlockSync","params":[]}],"meta":[]}]}],"entry":[],"exit":[],"initial":"","states":[]},{"name":"prestart","after":[{"name":"500","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.started","guard":null,"actions":[],"meta":[]}]}],"always":[],"on":[],"entry":[],"exit":[],"initial":"","states":[]},{"name":"started","after":[],"always":[],"on":[{"name":"newTransaction","transitions":[{"target":"","guard":{"type":"ifNewTransaction","params":[]},"actions":[{"type":"addToMempool","params":[]},{"type":"registerWithKayros","params":[]},{"type":"sendNewTransactionResponse","params":[]},{"type":"forwardMsgToChat","params":[{"key":"protocolId","value":"mempool"}]}],"meta":[]}]},{"name":"receiveStateSyncRequest","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveStateSyncRequest","params":[]}],"meta":[]}]},{"name":"updateNode","transitions":[{"target":"","guard":null,"actions":[{"type":"updateNodeAndReturn","params":[]}],"meta":[]}]}],"entry":[],"exit":[],"initial":"Node","states":[{"name":"Node","after":[],"always":[{"name":"always","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.started.Validator","actions":[{"type":"registerValidatorWithNetwork","params":[]}],"guard":{"type":"ifNodeIsValidator","params":[]},"meta":[]}]}],"on":[{"name":"becomeValidator","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.started.Validator","guard":null,"actions":[{"type":"registerValidatorWithNetwork","params":[]},{"type":"requestBlockSync","params":[]}],"meta":[]}]},{"name":"receiveStateSyncResponse","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveStateSyncResponse","params":[]}],"meta":[]}]},{"name":"start","transitions":[{"target":"","guard":null,"actions":[{"type":"connectPeers","params":[]},{"type":"connectRooms","params":[]},{"type":"requestBlockSync","params":[]}],"meta":[]}]},{"name":"receiveCommit","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveCommit","params":[]}],"meta":[]}]},{"name":"receiveUpdateNodeResponse","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveUpdateNodeResponse","params":[]}],"meta":[]}]}],"entry":[],"exit":[],"initial":"","states":[]},{"name":"Validator","after":[],"always":[],"on":[{"name":"stop","transitions":[{"target":"#Kayros-P2P-OnDemand-1.stopped","guard":null,"actions":[],"meta":[]}]},{"name":"receiveUpdateNodeResponse","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveUpdateNodeResponse","params":[]}],"meta":[]}]},{"name":"start","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.started.Validator","guard":null,"actions":[{"type":"connectPeers","params":[]},{"type":"connectRooms","params":[]},{"type":"registerValidatorWithNetwork","params":[]},{"type":"requestBlockSync","params":[]}],"meta":[]}]},{"name":"receiveCommit","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveCommit","params":[]}],"meta":[]}]},{"name":"receiveUpdateNodeRequest","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveUpdateNodeRequest","params":[]}],"meta":[]}]},{"name":"receiveStateSyncResponse","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveStateSyncResponse","params":[]},{"type":"requestValidatorNodeInfoIfSynced","params":[]}],"meta":[]}]}],"entry":[],"exit":[],"initial":"active","states":[{"name":"active","after":[{"name":"timeoutMissingTxs","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.started.Validator.propose","guard":null,"actions":[],"meta":[]}]}],"always":[{"name":"always","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.started.Validator.propose","guard":{"type":"ifAllTransactions","params":[]},"actions":[],"meta":[]}]}],"on":[{"name":"receiveMissingTransactions","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.started.Validator.active","guard":null,"actions":[{"type":"receiveMissingTransactions","params":[]}],"meta":[]}]}],"entry":[{"type":"getKayrosTxs","params":[]},{"type":"cancelActiveIntervals","params":[{"key":"after","value":"timeoutMissingTxs"}]}],"exit":[],"initial":"","states":[]},{"name":"propose","after":[{"name":"timeoutCommit","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.started.waiting","guard":null,"actions":[],"meta":[]}]}],"always":[],"on":[],"entry":[{"type":"commitBlock","params":[]},{"type":"sendCommit","params":[]},{"type":"cancelActiveIntervals","params":[{"key":"after","value":"timeoutCommit"}]}],"exit":[],"initial":"","states":[]}]},{"name":"waiting","after":[{"name":"batchTimeout","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.started.Validator.active","guard":{"type":"ifMempoolNotEmpty","params":[]},"actions":[],"meta":[]}]}],"always":[],"on":[{"name":"newTransaction","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.started.waiting","guard":{"type":"ifOldTransaction","params":[]},"actions":[],"meta":[]},{"target":"#Kayros-P2P-OnDemand-1.initialized.started.waiting","guard":{"type":"ifMempoolEmpty","params":[]},"actions":[{"type":"addToMempool","params":[]},{"type":"registerWithKayros","params":[]},{"type":"sendNewTransactionResponse","params":[]},{"type":"forwardMsgToChat","params":[{"key":"protocolId","value":"mempool"}]}],"meta":[]},{"target":"#Kayros-P2P-OnDemand-1.initialized.started.waiting","guard":{"type":"ifMempoolFull","params":[]},"actions":[{"type":"addToMempool","params":[]},{"type":"registerWithKayros","params":[]},{"type":"sendNewTransactionResponse","params":[]},{"type":"forwardMsgToChat","params":[{"key":"protocolId","value":"mempool"}]},{"type":"cancelActiveIntervals","params":[{"key":"after","value":"batchTimeout"}]}],"meta":[]},{"target":"#Kayros-P2P-OnDemand-1.initialized.started.waiting","guard":null,"actions":[{"type":"addToMempool","params":[]},{"type":"registerWithKayros","params":[]},{"type":"sendNewTransactionResponse","params":[{"key":"protocolId","value":"mempool"}]},{"type":"forwardMsgToChat","params":[{"key":"protocolId","value":"mempool"}]}],"meta":[]}]}],"entry":[],"exit":[],"initial":"","states":[]}]}]},{"name":"stopped","after":[],"always":[],"on":[{"name":"restart","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.unstarted","guard":null,"actions":[],"meta":[]}]}],"entry":[],"exit":[],"initial":"","states":[]}]} diff --git a/tests/testdata/tinygo/wasmx-kayrosp2p-ondemand-lib/diagram/def.ts b/tests/testdata/tinygo/wasmx-kayrosp2p-ondemand-lib/diagram/def.ts new file mode 100644 index 00000000..5c272482 --- /dev/null +++ b/tests/testdata/tinygo/wasmx-kayrosp2p-ondemand-lib/diagram/def.ts @@ -0,0 +1,498 @@ +// @ts-nocheck +import { createMachine } from "xstate"; + +export const machine = createMachine({ + context: { + blockTimeout: "timeoutCommit", + data_type_id: "", + genesis_uuid: "", + max_tx_bytes: 65536, + max_block_gas: "20000000", + timeoutCommit: 4000, + kayros_base_url: "", + kayros_user_key: "", + threshold_commit: 51, + timeoutMissingTxs: 4000, + threshold_finalize: 75, + batchTimeout: 1000, + }, + id: "Kayros-P2P-OnDemand-1", + initial: "uninitialized", + states: { + uninitialized: { + on: { + initialize: { + target: "initialized", + }, + }, + }, + initialized: { + initial: "unstarted", + states: { + unstarted: { + on: { + setupNode: { + target: "unstarted", + actions: { + type: "setupNode", + }, + }, + prestart: { + target: "prestart", + }, + setup: { + target: "unstarted", + actions: { + type: "setup", + }, + }, + start: { + target: "started", + actions: [ + { + type: "connectPeers", + }, + { + type: "connectRooms", + }, + { + type: "requestBlockSync", + }, + ], + }, + }, + }, + prestart: { + after: { + "500": { + target: "started", + }, + }, + }, + started: { + initial: "Node", + on: { + newTransaction: { + actions: [ + { + type: "addToMempool", + }, + { + type: "registerWithKayros", + }, + { + type: "sendNewTransactionResponse", + }, + { + type: "forwardMsgToChat", + params: { + protocolId: "mempool", + }, + }, + ], + guard: { + type: "ifNewTransaction", + }, + }, + receiveStateSyncRequest: { + actions: { + type: "receiveStateSyncRequest", + }, + }, + updateNode: { + actions: { + type: "updateNodeAndReturn", + }, + }, + }, + states: { + Node: { + on: { + becomeValidator: { + target: "Validator", + actions: [ + { + type: "registerValidatorWithNetwork", + }, + { + type: "requestBlockSync", + }, + ], + }, + receiveStateSyncResponse: { + actions: { + type: "receiveStateSyncResponse", + }, + }, + start: { + actions: [ + { + type: "connectPeers", + }, + { + type: "connectRooms", + }, + { + type: "requestBlockSync", + }, + ], + }, + receiveCommit: { + actions: { + type: "receiveCommit", + }, + }, + receiveUpdateNodeResponse: { + actions: { + type: "receiveUpdateNodeResponse", + }, + }, + }, + always: { + target: "Validator", + actions: { + type: "registerValidatorWithNetwork", + }, + guard: { + type: "ifNodeIsValidator", + }, + }, + }, + Validator: { + initial: "active", + on: { + stop: { + target: "#Kayros-P2P-OnDemand-1.stopped", + }, + receiveUpdateNodeResponse: { + actions: { + type: "receiveUpdateNodeResponse", + }, + }, + start: { + target: "Validator", + actions: [ + { + type: "connectPeers", + }, + { + type: "connectRooms", + }, + { + type: "registerValidatorWithNetwork", + }, + { + type: "requestBlockSync", + }, + ], + }, + receiveCommit: { + actions: { + type: "receiveCommit", + }, + description: + "Receive the block hashes from other validators and finalize the block. Rollback if 2/3 with another hash.", + }, + receiveUpdateNodeRequest: { + actions: { + type: "receiveUpdateNodeRequest", + }, + }, + receiveStateSyncResponse: { + actions: [ + { + type: "receiveStateSyncResponse", + }, + { + type: "requestValidatorNodeInfoIfSynced", + }, + ], + }, + }, + states: { + active: { + on: { + receiveMissingTransactions: { + target: "active", + actions: { + type: "receiveMissingTransactions", + }, + }, + }, + after: { + timeoutMissingTxs: { + target: "propose", + }, + }, + always: { + target: "propose", + guard: { + type: "ifAllTransactions", + }, + }, + entry: [ + { + type: "getKayrosTxs", + }, + { + type: "cancelActiveIntervals", + params: { + after: "timeoutMissingTxs", + }, + }, + ], + }, + propose: { + after: { + timeoutCommit: { + target: + "#Kayros-P2P-OnDemand-1.initialized.started.waiting", + }, + }, + entry: [ + { + type: "commitBlock", + }, + { + type: "sendCommit", + }, + { + type: "cancelActiveIntervals", + params: { + after: "timeoutCommit", + }, + }, + ], + }, + }, + }, + waiting: { + on: { + newTransaction: [ + { + target: "waiting", + guard: { + type: "ifOldTransaction", + }, + }, + { + target: "waiting", + actions: [ + { + type: "addToMempool", + }, + { + type: "registerWithKayros", + }, + { + type: "sendNewTransactionResponse", + }, + { + type: "forwardMsgToChat", + params: { + protocolId: "mempool", + }, + }, + ], + guard: { + type: "ifMempoolEmpty", + }, + }, + { + target: "waiting", + actions: [ + { + type: "addToMempool", + }, + { + type: "registerWithKayros", + }, + { + type: "sendNewTransactionResponse", + }, + { + type: "forwardMsgToChat", + params: { + protocolId: "mempool", + }, + }, + { + type: "cancelActiveIntervals", + params: { + after: "batchTimeout", + }, + }, + ], + guard: { + type: "ifMempoolFull", + }, + }, + { + target: "waiting", + actions: [ + { + type: "addToMempool", + }, + { + type: "registerWithKayros", + }, + { + type: "sendNewTransactionResponse", + params: { + protocolId: "mempool", + }, + }, + { + type: "forwardMsgToChat", + params: { + protocolId: "mempool", + }, + }, + ], + }, + ], + }, + after: { + batchTimeout: { + target: + "#Kayros-P2P-OnDemand-1.initialized.started.Validator.active", + guard: { + type: "ifMempoolNotEmpty", + }, + }, + }, + }, + }, + }, + }, + }, + stopped: { + on: { + restart: { + target: "#Kayros-P2P-OnDemand-1.initialized.unstarted", + }, + }, + }, + }, +}).withConfig({ + actions: { + getKayrosTxs: function (context, event) { + // Add your action code here + // ... + }, + cancelActiveIntervals: function (context, event) { + // Add your action code here + // ... + }, + commitBlock: function (context, event) { + // Add your action code here + // ... + }, + sendCommit: function (context, event) { + // Add your action code here + // ... + }, + setupNode: function (context, event) { + // Add your action code here + // ... + }, + addToMempool: function (context, event) { + // Add your action code here + // ... + }, + registerWithKayros: function (context, event) { + // Add your action code here + // ... + }, + sendNewTransactionResponse: function (context, event) { + // Add your action code here + // ... + }, + forwardMsgToChat: function (context, event) { + // Add your action code here + // ... + }, + setup: function (context, event) { + // Add your action code here + // ... + }, + receiveUpdateNodeResponse: function (context, event) { + // Add your action code here + // ... + }, + receiveStateSyncRequest: function (context, event) { + // Add your action code here + // ... + }, + updateNodeAndReturn: function (context, event) { + // Add your action code here + // ... + }, + registerValidatorWithNetwork: function (context, event) { + // Add your action code here + // ... + }, + requestBlockSync: function (context, event) { + // Add your action code here + // ... + }, + receiveStateSyncResponse: function (context, event) { + // Add your action code here + // ... + }, + connectPeers: function (context, event) { + // Add your action code here + // ... + }, + connectRooms: function (context, event) { + // Add your action code here + // ... + }, + receiveCommit: function (context, event) { + // Add your action code here + // ... + }, + receiveUpdateNodeRequest: function (context, event) { + // Add your action code here + // ... + }, + requestValidatorNodeInfoIfSynced: function (context, event) { + // Add your action code here + // ... + }, + receiveMissingTransactions: function (context, event) { + // Add your action code here + // ... + }, + }, + guards: { + ifNewTransaction: function (context, event) { + // Add your guard condition here + return true; + }, + ifNodeIsValidator: function (context, event) { + // Add your guard condition here + return true; + }, + ifAllTransactions: function (context, event) { + // Add your guard condition here + return true; + }, + ifOldTransaction: function (context, event) { + // Add your guard condition here + return true; + }, + ifMempoolEmpty: function (context, event) { + // Add your guard condition here + return true; + }, + ifMempoolFull: function (context, event) { + // Add your guard condition here + return true; + }, + ifMempoolNotEmpty: function (context, event) { + // Add your guard condition here + return true; + }, + }, +}); diff --git a/tests/testdata/tinygo/wasmx-kayrosp2p-ondemand-lib/go.mod b/tests/testdata/tinygo/wasmx-kayrosp2p-ondemand-lib/go.mod new file mode 100644 index 00000000..99ddb601 --- /dev/null +++ b/tests/testdata/tinygo/wasmx-kayrosp2p-ondemand-lib/go.mod @@ -0,0 +1,72 @@ +module github.com/loredanacirstea/wasmx-kayrosp2p-ondemand-lib + +go 1.24 + +toolchain go1.24.4 + +require github.com/loredanacirstea/wasmx-env v0.0.0 + +require github.com/loredanacirstea/wasmx-fsm v0.0.0 + +require github.com/loredanacirstea/wasmx-env-core v0.0.0 // indirect + +require github.com/loredanacirstea/wasmx-env-consensus v0.0.0 // indirect + +require github.com/loredanacirstea/wasmx-env-crosschain v0.0.0 // indirect + +require github.com/loredanacirstea/wasmx-staking v0.0.0 // indirect + +require github.com/loredanacirstea/wasmx-consensus-utils v0.0.0 // indirect + +require github.com/loredanacirstea/wasmx-blocks v0.0.0 // indirect + +require github.com/loredanacirstea/wasmx-env-multichain v0.0.0 // indirect + +require github.com/loredanacirstea/wasmx-raft-lib v0.0.0 + +require github.com/loredanacirstea/wasmx-raftp2p-lib v0.0.0 + +require github.com/loredanacirstea/wasmx-kayrosp2p-lib v0.0.0 + +require ( + cosmossdk.io/math v1.5.3 // indirect + github.com/loredanacirstea/wasmx-env-httpclient v0.0.0 // indirect + github.com/loredanacirstea/wasmx-env-p2p v0.0.0 // indirect + github.com/loredanacirstea/wasmx-env-utils v0.0.0 // indirect + github.com/loredanacirstea/wasmx-kayros-verifier v0.0.0 // indirect + github.com/loredanacirstea/wasmx-utils v0.0.0 // indirect +) + +replace github.com/loredanacirstea/wasmx-env v0.0.0 => ../wasmx-env + +replace github.com/loredanacirstea/wasmx-env-utils v0.0.0 => ../wasmx-env-utils + +replace github.com/loredanacirstea/wasmx-utils v0.0.0 => ../wasmx-utils + +replace github.com/loredanacirstea/wasmx-fsm v0.0.0 => ../wasmx-fsm + +replace github.com/loredanacirstea/wasmx-env-core v0.0.0 => ../wasmx-env-core + +replace github.com/loredanacirstea/wasmx-env-crosschain v0.0.0 => ../wasmx-env-crosschain + +replace github.com/loredanacirstea/wasmx-env-consensus v0.0.0 => ../wasmx-env-consensus + +replace github.com/loredanacirstea/wasmx-staking v0.0.0 => ../wasmx-staking + +replace github.com/loredanacirstea/wasmx-consensus-utils v0.0.0 => ../wasmx-consensus-utils + +replace github.com/loredanacirstea/wasmx-blocks v0.0.0 => ../wasmx-blocks + +replace github.com/loredanacirstea/wasmx-env-multichain v0.0.0 => ../wasmx-env-multichain + +replace github.com/loredanacirstea/wasmx-raft-lib v0.0.0 => ../wasmx-raft-lib + +replace github.com/loredanacirstea/wasmx-raftp2p-lib v0.0.0 => ../wasmx-raftp2p-lib + +replace github.com/loredanacirstea/wasmx-env-p2p v0.0.0 => ../wasmx-env-p2p + +replace github.com/loredanacirstea/wasmx-env-httpclient v0.0.0 => ../wasmx-env-httpclient + +replace github.com/loredanacirstea/wasmx-kayrosp2p-lib v0.0.0 => ../wasmx-kayrosp2p-lib + +replace github.com/loredanacirstea/wasmx-kayros-verifier v0.0.0 => ../wasmx-kayros-verifier diff --git a/tests/testdata/tinygo/wasmx-kayrosp2p-ondemand-lib/go.sum b/tests/testdata/tinygo/wasmx-kayrosp2p-ondemand-lib/go.sum new file mode 100644 index 00000000..18cf05fc --- /dev/null +++ b/tests/testdata/tinygo/wasmx-kayrosp2p-ondemand-lib/go.sum @@ -0,0 +1,12 @@ +cosmossdk.io/math v1.5.3 h1:WH6tu6Z3AUCeHbeOSHg2mt9rnoiUWVWaQ2t6Gkll96U= +cosmossdk.io/math v1.5.3/go.mod h1:uqcZv7vexnhMFJF+6zh9EWdm/+Ylyln34IvPnBauPCQ= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/tests/testdata/tinygo/wasmx-kayrosp2p-ondemand-lib/lib/actions.go b/tests/testdata/tinygo/wasmx-kayrosp2p-ondemand-lib/lib/actions.go new file mode 100644 index 00000000..7746f56c --- /dev/null +++ b/tests/testdata/tinygo/wasmx-kayrosp2p-ondemand-lib/lib/actions.go @@ -0,0 +1,105 @@ +package lib + +import ( + "encoding/base64" + "fmt" + + wasmx "github.com/loredanacirstea/wasmx-env/lib" + fsm "github.com/loredanacirstea/wasmx-fsm/lib" + raftlib "github.com/loredanacirstea/wasmx-raft-lib/lib" +) + +// IfMempoolEmpty checks if the mempool has no transactions +func IfMempoolEmpty(params []fsm.ActionParam, event fsm.EventObject) (bool, error) { + mempool, err := raftlib.GetMempool() + if err != nil { + return false, err + } + return len(mempool.Map) == 0, nil +} + +// IfMempoolNotEmpty checks if the mempool has at least one transaction +func IfMempoolNotEmpty(params []fsm.ActionParam, event fsm.EventObject) (bool, error) { + isEmpty, err := IfMempoolEmpty(params, event) + if err != nil { + return false, err + } + return !isEmpty, nil +} + +// IfNewTransaction checks if a transaction is new (not already in mempool) +func IfNewTransaction(params []fsm.ActionParam, event fsm.EventObject) (bool, error) { + // Extract base64 transaction + txB64 := "" + if len(event.Params) > 0 { + for _, p := range event.Params { + if p.Key == "transaction" { + txB64 = p.Value + break + } + } + } + if txB64 == "" { + for _, p := range params { + if p.Key == "transaction" { + txB64 = p.Value + break + } + } + } + if txB64 == "" { + return false, fmt.Errorf("no transaction found") + } + + // Decode transaction from base64 and compute hash + txBytes, err := base64.StdEncoding.DecodeString(txB64) + if err != nil { + return false, err + } + txhash := base64.StdEncoding.EncodeToString(wasmx.Sha256(txBytes)) + + // Get mempool + mp, err := raftlib.GetMempool() + if err != nil { + return false, err + } + + // Check if transaction has been seen + existent := mp.HasSeen(txhash) + if existent { + LoggerDebug("mempool: transaction already added or seen", []string{"txhash", txhash}) + } + + return !existent, nil +} + +// IfOldTransaction checks if a transaction is already known (not new) +func IfOldTransaction(params []fsm.ActionParam, event fsm.EventObject) (bool, error) { + isNew, err := IfNewTransaction(params, event) + if err != nil { + return false, err + } + return !isNew, nil +} + +// IfMempoolFull checks if the mempool batch is full based on max_gas and max_bytes +func IfMempoolFull(params []fsm.ActionParam, event fsm.EventObject) (bool, error) { + mempool, err := raftlib.GetMempool() + if err != nil { + return false, err + } + + // Get consensus params to determine max gas and max bytes + consensusParams, err := raftlib.GetConsensusParams(0) + if err != nil { + return false, err + } + + maxBytes := consensusParams.Block.MaxBytes + if maxBytes == -1 { + maxBytes = raftlib.MaxBlockSizeBytes + } + + // Check if mempool batch is full + return mempool.IsBatchFull(consensusParams.Block.MaxGas, maxBytes), nil +} diff --git a/tests/testdata/tinygo/wasmx-kayrosp2p-ondemand-lib/lib/types.go b/tests/testdata/tinygo/wasmx-kayrosp2p-ondemand-lib/lib/types.go new file mode 100644 index 00000000..265a7775 --- /dev/null +++ b/tests/testdata/tinygo/wasmx-kayrosp2p-ondemand-lib/lib/types.go @@ -0,0 +1,7 @@ +package lib + +// Module identification and protocol constants +const ( + MODULE_NAME = "kayrosp2p_ondemand" + PROTOCOL_ID = "kayrosp2p_ondemand_1" +) diff --git a/tests/testdata/tinygo/wasmx-kayrosp2p-ondemand-lib/lib/utils.go b/tests/testdata/tinygo/wasmx-kayrosp2p-ondemand-lib/lib/utils.go new file mode 100644 index 00000000..284f4899 --- /dev/null +++ b/tests/testdata/tinygo/wasmx-kayrosp2p-ondemand-lib/lib/utils.go @@ -0,0 +1,25 @@ +package lib + +import ( + wasmx "github.com/loredanacirstea/wasmx-env/lib" +) + +func LoggerInfo(msg string, parts []string) { + wasmx.LoggerInfo(MODULE_NAME, msg, parts) +} + +func LoggerError(msg string, parts []string) { + wasmx.LoggerError(MODULE_NAME, msg, parts) +} + +func LoggerDebug(msg string, parts []string) { + wasmx.LoggerDebug(MODULE_NAME, msg, parts) +} + +func LoggerDebugExtended(msg string, parts []string) { + wasmx.LoggerDebugExtended(MODULE_NAME, msg, parts) +} + +func Revert(message string) { + wasmx.RevertWithModule(MODULE_NAME, message) +} diff --git a/tests/testdata/tinygo/wasmx-raft-lib/lib/types_blockchain.go b/tests/testdata/tinygo/wasmx-raft-lib/lib/types_blockchain.go index 6fad3c56..11a6df2a 100644 --- a/tests/testdata/tinygo/wasmx-raft-lib/lib/types_blockchain.go +++ b/tests/testdata/tinygo/wasmx-raft-lib/lib/types_blockchain.go @@ -91,6 +91,25 @@ func (m *Mempool) HasSeen(txhash string) bool { return ok } +func (m *Mempool) IsRecentlyProcessed(txhash string) bool { + if m == nil { + return false + } + _, ok := m.Map[txhash] + if m.Temp[txhash] && !ok { + return true + } + return false +} + +func (m *Mempool) IsInMempool(txhash string) bool { + if m == nil { + return false + } + _, ok := m.Map[txhash] + return ok +} + func (m *Mempool) Seen(txhash string) { if m == nil { return diff --git a/tests/utils/utils.go b/tests/utils/utils.go index 329ca9f2..8767ca02 100644 --- a/tests/utils/utils.go +++ b/tests/utils/utils.go @@ -6,6 +6,7 @@ import ( "os" "path" "runtime" + "strings" "time" "cosmossdk.io/math" @@ -46,7 +47,19 @@ func SystemContractsModify(wasmRuntime string) func([]wasmxtypes.SystemContract) } else { contracts[i].Pinned = false } + } + } + for i := range contracts { + if strings.Contains(contracts[i].Label, "kayrosp2p") { + execmsg := &wasmxtypes.WasmxExecutionMessage{} + json.Unmarshal(contracts[i].InitMessage, execmsg) + + msg := string(execmsg.Data) + msg = strings.Replace(msg, `{"key":"data_type_id","value":""}`, fmt.Sprintf(`{"key":"data_type_id","value":"%d"}`, time.Now().Unix()), 1) + execmsg.Data = []byte(msg) + execmsgbz, _ := json.Marshal(execmsg) + contracts[i].InitMessage = execmsgbz } } return contracts diff --git a/tests/wasmx/keeper_test.go b/tests/wasmx/keeper_test.go index 3a6cc9a1..2215d123 100644 --- a/tests/wasmx/keeper_test.go +++ b/tests/wasmx/keeper_test.go @@ -14,6 +14,7 @@ import ( dbm "github.com/cosmos/cosmos-db" wt "github.com/loredanacirstea/wasmx/testutil/wasmx" + "github.com/loredanacirstea/wasmx/x/vmhttpclient" // wasmedge "github.com/loredanacirstea/wasmx-wasmedge" wazero "github.com/loredanacirstea/wasmx-wazero" @@ -27,6 +28,10 @@ var ( runKnownFixme bool ) +func init() { + vmhttpclient.Setup() +} + // TestMain is the main entry point for the tests. func TestMain(m *testing.M) { // Add custom flags diff --git a/wasmx/EXAMPLES.md b/wasmx/EXAMPLES.md index 1140663a..771d2b1c 100644 --- a/wasmx/EXAMPLES.md +++ b/wasmx/EXAMPLES.md @@ -6,7 +6,7 @@ * on-demand blocks ```bash -mythosd testnet init-files --network.initial-chains=ondemand_single --v 1 --output-dir=$(pwd)/testnet --minimum-gas-prices="1000amyt" --nocors --libp2p --enable-eid=false --chain-id=mythos_7000-14 +mythosd testnet init-files --network.initial-chains=mythos --consensus-label=ondemand_single_0.0.1 --v 1 --output-dir=$(pwd)/testnet --minimum-gas-prices="1000amyt" --nocors --libp2p --enable-eid=false --chain-id=mythos_7000-14 ``` ## Basic chain, 1 validator @@ -39,7 +39,7 @@ mythosd tx wasmx store ./tests/testdata/wasmx/simple_storage.wasm --chain-id=myt # mythosd query tx # search code_id -mythosd tx wasmx instantiate 57 '{"crosschain_contract":"metaregistry"}' --label "simple_storage" --chain-id=mythos_7000-14 --from=node0 --keyring-backend=test --home=./testnet/node0/mythosd --fees=90000000000amyt --gas=10000000 --node tcp://localhost:26657 --yes +mythosd tx wasmx instantiate 61 '{"crosschain_contract":"metaregistry"}' --label "simple_storage" --chain-id=mythos_7000-14 --from=node0 --keyring-backend=test --home=./testnet/node0/mythosd --fees=90000000000amyt --gas=10000000 --node tcp://localhost:26657 --yes # mythosd query tx # search contract_address diff --git a/wasmx/app/app.go b/wasmx/app/app.go index 0de79921..e379a380 100644 --- a/wasmx/app/app.go +++ b/wasmx/app/app.go @@ -169,6 +169,7 @@ import ( docs "github.com/loredanacirstea/wasmx/docs" networkmodule "github.com/loredanacirstea/wasmx/x/network" + "github.com/loredanacirstea/wasmx/x/vmhttpclient" networkmodulekeeper "github.com/loredanacirstea/wasmx/x/network/keeper" @@ -261,7 +262,7 @@ func init() { vmkv.Setup() // vmimap.Setup() // vmsmtp.Setup() - // vmhttpclient.Setup() + vmhttpclient.Setup() // vmhttpserver.Setup() // vmoauth2client.Setup() } diff --git a/wasmx/cmdutils/testnet.go b/wasmx/cmdutils/testnet.go index 556a56fe..b5638036 100644 --- a/wasmx/cmdutils/testnet.go +++ b/wasmx/cmdutils/testnet.go @@ -93,6 +93,7 @@ var ( flagP2P = "libp2p" flagMinLevelValidators = "min-level-validators" flagEnableEIDCheck = "enable-eid" + flagConsensusLabel = "consensus-label" ) type initArgs struct { @@ -111,6 +112,7 @@ type initArgs struct { minLevelValidators int enableEIDCheck bool initialChains []string + consensusLabel string } type startArgs struct { @@ -132,7 +134,8 @@ type startArgs struct { jsonRpcAddress string jsonRpcWsAddress string - p2p bool + p2p bool + consensusLabel string } func addTestnetFlagsToCmd(cmd *cobra.Command) { @@ -144,6 +147,7 @@ func addTestnetFlagsToCmd(cmd *cobra.Command) { cmd.Flags().Bool(flagP2P, false, "wether the consensus algorithm uses libp2p or not") cmd.Flags().Int(flagMinLevelValidators, 2, "minimum number of validators for chain levels") cmd.Flags().Bool(flagEnableEIDCheck, false, "enable eID checks") + cmd.Flags().String(flagConsensusLabel, "", "Consensus contract label (role consensus primary)") } // NewTestnetCmd creates a root testnet command with subcommands to run an in-process testnet or initialize @@ -181,7 +185,7 @@ Note, strict routability for addresses is turned off in the config file. Example: mythosd testnet init-files --network.initial-chains=mythos,level0 --v 4 --output-dir ./.testnets --starting-ip-address 192.168.10.2 - mythosd testnet init-files --network.initial-chains=ondemand_single --v 1 --output-dir=$(pwd)/testnet --minimum-gas-prices="1000amyt" --nocors --libp2p --enable-eid=false --chain-id=mythos_7000-14 + mythosd testnet init-files --network.initial-chains=mythos --v 1 --output-dir=$(pwd)/testnet --minimum-gas-prices="1000amyt" --nocors --libp2p --enable-eid=false --chain-id=mythos_7000-14 `, RunE: func(cmd *cobra.Command, _ []string) error { @@ -207,6 +211,7 @@ Example: args.p2p, _ = cmd.Flags().GetBool(flagP2P) args.minLevelValidators, _ = cmd.Flags().GetInt(flagMinLevelValidators) args.enableEIDCheck, _ = cmd.Flags().GetBool(flagEnableEIDCheck) + args.consensusLabel, _ = cmd.Flags().GetString(flagConsensusLabel) args.initialChains, _ = cmd.Flags().GetStringSlice(networksrvflags.NetworkInitialChains) return initTestnetFiles(wasmVmMeta, clientCtx, cmd, serverCtx.Config, mbm, genBalIterator, args) }, @@ -219,7 +224,7 @@ Example: cmd.Flags().Bool(flagSameMachine, false, "Starting nodes on the same machine, on different ports") cmd.Flags().Bool(flagNoCors, false, "If present, sets cors to *") cmd.Flags().String(flags.FlagKeyringBackend, flags.DefaultKeyringBackend, "Select keyring's backend (os|file|test)") - cmd.Flags().StringSlice(networksrvflags.NetworkInitialChains, []string{"mythos"}, "Initialized chains, separated by comma. E.g. 'mythos', 'mythos,level0', 'ondemand_single'") + cmd.Flags().StringSlice(networksrvflags.NetworkInitialChains, []string{"mythos"}, "Initialized chains, separated by comma. E.g. 'mythos', 'mythos,level0'") return cmd } @@ -274,7 +279,7 @@ Example: cmd.Flags().Bool(flagSameMachine, false, "Starting nodes on the same machine, on different ports") cmd.Flags().Bool(flagNoCors, false, "If present, sets cors to *") cmd.Flags().String(flags.FlagKeyringBackend, flags.DefaultKeyringBackend, "Select keyring's backend (os|file|test)") - cmd.Flags().StringSlice(networksrvflags.NetworkInitialChains, []string{"mythos"}, "Initialized chains, separated by comma. E.g. 'mythos', 'mythos,level0', 'ondemand_single'") + cmd.Flags().StringSlice(networksrvflags.NetworkInitialChains, []string{"mythos"}, "Initialized chains, separated by comma. E.g. 'mythos', 'mythos,level0'") return cmd } @@ -488,7 +493,6 @@ func initTestnetFilesInternal( mockNodeHome := path.Join(args.outputDir, "tmp") generateMythos := slices.Contains(args.initialChains, "mythos") generateLevel0 := slices.Contains(args.initialChains, "level0") - generateOnDemandSingle := slices.Contains(args.initialChains, "ondemand_single") if args.chainID == "" { args.chainID = fmt.Sprintf("mythos_%d-1", tmrand.Int63n(9999999999999)+1) } @@ -526,9 +530,6 @@ func initTestnetFilesInternal( if generateLevel0 { initialChainIds = append(initialChainIds, chainId0) } - if generateOnDemandSingle { - initialChainIds = append(initialChainIds, chainId) - } nodeIDs := make([]string, args.numValidators) valPubKeys := make([]cryptotypes.PubKey, args.numValidators) nodeIPs := make([]string, args.numValidators) @@ -749,7 +750,7 @@ func initTestnetFilesInternal( if nodeIndexStart == 0 { if generateMythos { - if err := initGenFiles(clientCtx, mbm, args.chainID, genAccounts, genBalances, genFiles, args.numValidators, args.minLevelValidators, args.enableEIDCheck); err != nil { + if err := initGenFiles(clientCtx, mbm, args.chainID, genAccounts, genBalances, genFiles, args.numValidators, args.minLevelValidators, args.enableEIDCheck, args.consensusLabel); err != nil { return err } @@ -821,7 +822,7 @@ func initTestnetFilesInternal( genFile := strings.Replace(genFiles[i], ".json", "_"+chainId0+".json", 1) - if err := initGenFilesLevel0(clientCtx, mbm, chainId0, genAccount, genBalance, genFile, 1, args.minLevelValidators, args.enableEIDCheck); err != nil { + if err := initGenFilesLevel0(clientCtx, mbm, chainId0, genAccount, genBalance, genFile, 1, args.minLevelValidators, args.enableEIDCheck, ""); err != nil { return err } err = collectGenFiles( @@ -836,26 +837,6 @@ func initTestnetFilesInternal( } } - if generateOnDemandSingle { - // initialize on-demand-single chain with same setup as mythos but different system contracts - if err := initGenFilesOnDemandSingle(clientCtx, mbm, args.chainID, genAccounts, genBalances, genFiles, args.numValidators, args.minLevelValidators, args.enableEIDCheck); err != nil { - return err - } - - for i := nodeIndexStart; i < args.numValidators; i++ { - err = collectGenFiles( - clientCtx, - mythosapp.TxConfig(), - nodeConfig, - args.chainID, nodeIDs[i], valPubKeys[i], i, - args.outputDir, args.nodeDirPrefix, args.nodeDaemonHome, genBalIterator, valAddrCodec, args.sameMachine, nodeConfig.GenesisFile(), "gentxs", - ) - if err != nil { - return err - } - } - } - // cleanup mocks err = os.RemoveAll(mockNodeHome) if err != nil { @@ -938,6 +919,7 @@ func initGenFiles( numValidators int, minLevelValidators int, enableEIDCheck bool, + consensusLabel string, ) error { appGenState := mbm.DefaultGenesis(clientCtx.Codec) chaincfg, err := mcfg.GetChainConfig(chainID) @@ -1006,6 +988,28 @@ func initGenFiles( clientCtx.Codec.MustUnmarshalJSON(appGenState[wasmxtypes.ModuleName], &wasmxGenState) wasmxGenState.SystemContracts = wasmxtypes.DefaultSystemContracts(addrCodec.(mcodec.AccBech32Codec), feeCollectorBech32, mintAddressBech32, int32(minLevelValidators), enableEIDCheck, "{}", mcfg.BondBaseDenom) wasmxGenState.BootstrapAccountAddress = bootstrapAccount + if consensusLabel != "" { + var found bool + for i := range wasmxGenState.SystemContracts { + role := wasmxGenState.SystemContracts[i].Role + if role == nil || role.Role != wasmxtypes.ROLE_CONSENSUS { + continue + } + if role.Label == consensusLabel { + role.Primary = true + found = true + } else { + role.Primary = false + } + } + if !found { + return fmt.Errorf("consensus-label not found in system contracts: %s", consensusLabel) + } + wasmxGenState.SystemContracts, err = wasmxtypes.FillRoles(wasmxGenState.SystemContracts, addrCodec.(mcodec.AccBech32Codec), feeCollectorBech32) + if err != nil { + return err + } + } appGenState[wasmxtypes.ModuleName] = clientCtx.Codec.MustMarshalJSON(&wasmxGenState) appGenStateJSON, err := json.MarshalIndent(appGenState, "", " ") @@ -1038,6 +1042,7 @@ func initGenFilesLevel0( numValidators int, minLevelValidators int, enableEIDCheck bool, + consensusLabel string, ) error { chaincfg, err := mcfg.GetChainConfig(chainID) if err != nil { @@ -1065,6 +1070,28 @@ func initGenFilesLevel0( clientCtx.Codec.MustUnmarshalJSON(appGenState[wasmxtypes.ModuleName], &wasmxGenState) wasmxGenState.SystemContracts = wasmxtypes.DefaultTimeChainContracts(addrCodec.(mcodec.AccBech32Codec), feeCollectorBech32, mintAddressBech32, int32(minLevelValidators), enableEIDCheck, "{}", chaincfg.BondBaseDenom) wasmxGenState.BootstrapAccountAddress = bootstrapAccount + if consensusLabel != "" { + var found bool + for i := range wasmxGenState.SystemContracts { + role := wasmxGenState.SystemContracts[i].Role + if role == nil || role.Role != wasmxtypes.ROLE_CONSENSUS { + continue + } + if role.Label == consensusLabel { + role.Primary = true + found = true + } else { + role.Primary = false + } + } + if !found { + return fmt.Errorf("consensus-label not found in system contracts: %s", consensusLabel) + } + wasmxGenState.SystemContracts, err = wasmxtypes.FillRoles(wasmxGenState.SystemContracts, addrCodec.(mcodec.AccBech32Codec), feeCollectorBech32) + if err != nil { + return err + } + } appGenState[wasmxtypes.ModuleName] = clientCtx.Codec.MustMarshalJSON(&wasmxGenState) var cosmosmodGenState cosmosmodtypes.GenesisState @@ -1120,103 +1147,6 @@ func initGenFilesLevel0( return nil } -func initGenFilesOnDemandSingle( - clientCtx client.Context, - mbm module.BasicManager, - chainID string, - genAccounts []cosmosmodtypes.GenesisAccount, - genBalances []banktypes.Balance, - genFiles []string, - numValidators int, - minLevelValidators int, - enableEIDCheck bool, -) error { - appGenState := mbm.DefaultGenesis(clientCtx.Codec) - chaincfg, err := mcfg.GetChainConfig(chainID) - if err != nil { - panic(err) - } - - addrCodec := mcodec.NewAccBech32Codec(chaincfg.Bech32PrefixAccAddr, mcodec.NewAddressPrefixedFromAcc) - - var cosmosmodGenState cosmosmodtypes.GenesisState - clientCtx.Codec.MustUnmarshalJSON(appGenState[cosmosmodtypes.ModuleName], &cosmosmodGenState) - - cosmosmodGenState.Bank.DenomInfo = cosmosmodtypes.DefaultBankDenoms(addrCodec.(mcodec.AccBech32Codec), mcfg.DenomUnit, uint32(mcfg.BaseDenomUnit)) - cosmosmodGenState.Bank.Balances = genBalances - cosmosmodGenState.Staking.Params.BondDenom = mcfg.BondBaseDenom - cosmosmodGenState.Staking.BaseDenom = mcfg.BaseDenom - p, _ := math.LegacyNewDecFromStr("0.6") - cosmosmodGenState.Slashing.Params.MinSignedPerWindow = p - cosmosmodGenState.Slashing.Params.DowntimeJailDuration = time.Hour * 2 - cosmosmodGenState.Slashing.Params.SignedBlocksWindow = 40000 - cosmosmodGenState.Distribution.BaseDenom = mcfg.BaseDenom - cosmosmodGenState.Distribution.RewardsDenom = cosmosmodGenState.Bank.DenomInfo[2].Metadata.Base - cosmosmodGenState.Gov.Params.MinDeposit[0].Denom = mcfg.BaseDenom - cosmosmodGenState.Gov.Params.ExpeditedMinDeposit = sdk.NewCoins(sdk.NewCoin(mcfg.BaseDenom, math.NewInt(50000000))) - votingP := time.Minute * 2 - cosmosmodGenState.Gov.Params.VotingPeriod = votingP.Milliseconds() - - // set the accounts in the genesis state - authGenesis, err := cosmosmodtypes.NewAuthGenesisStateFromCosmos(clientCtx.Codec, cosmosmodGenState.Auth.Params, genAccounts) - if err != nil { - return err - } - cosmosmodGenState.Auth = *authGenesis - - // set cosmosmod genesis - appGenState[cosmosmodtypes.ModuleName] = clientCtx.Codec.MustMarshalJSON(&cosmosmodGenState) - - var crisisGenState crisistypes.GenesisState - clientCtx.Codec.MustUnmarshalJSON(appGenState[crisistypes.ModuleName], &crisisGenState) - crisisGenState.ConstantFee.Denom = mcfg.BaseDenom - appGenState[crisistypes.ModuleName] = clientCtx.Codec.MustMarshalJSON(&crisisGenState) - - var mintGenState minttypes.GenesisState - clientCtx.Codec.MustUnmarshalJSON(appGenState[minttypes.ModuleName], &mintGenState) - mintGenState.Params.MintDenom = mcfg.BaseDenom - appGenState[minttypes.ModuleName] = clientCtx.Codec.MustMarshalJSON(&mintGenState) - - feeCollectorBech32, err := addrCodec.BytesToString(cosmosmodtypes.NewModuleAddress(wasmxtypes.FEE_COLLECTOR)) - if err != nil { - panic(err) - } - mintAddressBech32, err := addrCodec.BytesToString(cosmosmodtypes.NewModuleAddress("mint")) - if err != nil { - panic(err) - } - - bootstrapAccount, err := addrCodec.BytesToString(sdk.AccAddress(rand.Bytes(address.Len))) - if err != nil { - panic(err) - } - - var wasmxGenState wasmxtypes.GenesisState - clientCtx.Codec.MustUnmarshalJSON(appGenState[wasmxtypes.ModuleName], &wasmxGenState) - wasmxGenState.SystemContracts = wasmxtypes.DefaultOnDemandSingleContracts(addrCodec.(mcodec.AccBech32Codec), feeCollectorBech32, mintAddressBech32, int32(minLevelValidators), enableEIDCheck, "{}", mcfg.BondBaseDenom) - wasmxGenState.BootstrapAccountAddress = bootstrapAccount - appGenState[wasmxtypes.ModuleName] = clientCtx.Codec.MustMarshalJSON(&wasmxGenState) - - appGenStateJSON, err := json.MarshalIndent(appGenState, "", " ") - if err != nil { - return err - } - - genDoc := types.GenesisDoc{ - ChainID: chainID, - AppState: appGenStateJSON, - Validators: nil, - } - - // generate empty genesis files for each validator and save - for i := 0; i < numValidators; i++ { - if err := genDoc.SaveAs(genFiles[i]); err != nil { - return err - } - } - return nil -} - func collectGenFiles( clientCtx client.Context, txConfig client.TxConfig, diff --git a/wasmx/server/config/config.go b/wasmx/server/config/config.go index 46ca88d6..7eb29305 100644 --- a/wasmx/server/config/config.go +++ b/wasmx/server/config/config.go @@ -148,6 +148,7 @@ func GetConfig(v *viper.Viper) (Config, error) { Ips: v.GetString("network.ips"), Id: v.GetString("network.id"), InitialChains: v.GetStringSlice(networkflags.NetworkInitialChains), + StartEnv: v.GetString("network.start-env"), } return Config{ diff --git a/wasmx/server/start.go b/wasmx/server/start.go index e77dee6d..83da0e4c 100644 --- a/wasmx/server/start.go +++ b/wasmx/server/start.go @@ -579,7 +579,11 @@ func StartInProcess(wasmVmMeta memc.IWasmVmMeta, svrCtx *server.Context, clientC // start the node // TODO send the configs to the smart contract // TODO send cmsrvconfig, ctndcfg with StartNode hook - err = networkserver.StartNode(app, logger, app.GetNetworkKeeper()) + + // Read environment variables to pass to consensus + env := networkconfig.ParseStartEnv(msrvconfig.Network.StartEnv) + + err = networkserver.StartNode(app, logger, app.GetNetworkKeeper(), env) if err != nil { return err } diff --git a/wasmx/testutil/wasmx/chain.go b/wasmx/testutil/wasmx/chain.go index 3f1f4bed..52484666 100644 --- a/wasmx/testutil/wasmx/chain.go +++ b/wasmx/testutil/wasmx/chain.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" "encoding/json" + "flag" "fmt" "log" "net" @@ -71,6 +72,18 @@ import ( memc "github.com/loredanacirstea/wasmx/x/wasmx/vm/memory/common" ) +var ( + kayrosUserKey string + kayrosBaseURL string + consensusLabel string +) + +func init() { + flag.StringVar(&kayrosUserKey, "kayros_user_key", "", "Kayros indexer API key") + flag.StringVar(&kayrosBaseURL, "kayros_base_url", "", "Kayros base URL") + flag.StringVar(&consensusLabel, "consensus-label", "", "Consensus contract label (role consensus primary)") +} + // KeeperTestSuite is a testing suite to test keeper functions type KeeperTestSuite struct { suite.Suite @@ -80,6 +93,7 @@ type KeeperTestSuite struct { ChainIds []string CompiledCacheDir string MaxBlockGas int64 + StartNodeEnv map[string]string SystemContractsModify func([]wasmxtypes.SystemContract) []wasmxtypes.SystemContract GenesisModify func(genesisState map[string]json.RawMessage, app ibcgotesting.TestingApp) map[string]json.RawMessage GetDB func(dbpath string) dbm.DB @@ -214,6 +228,15 @@ func (suite *KeeperTestSuite) SetupChains() { if suite.MaxBlockGas > 0 { app.DefaultTestingConsensusParams.Block.MaxGas = suite.MaxBlockGas } + if suite.StartNodeEnv == nil && (kayrosUserKey != "" || kayrosBaseURL != "") { + suite.StartNodeEnv = map[string]string{} + if kayrosUserKey != "" { + suite.StartNodeEnv["kayros_user_key"] = kayrosUserKey + } + if kayrosBaseURL != "" { + suite.StartNodeEnv["kayros_base_url"] = kayrosBaseURL + } + } suite.Chains = map[string]*TestChain{} mcfg.ChainIdsInit = []string{ @@ -293,6 +316,28 @@ func (suite *KeeperTestSuite) SetupApp(chainId string, chaincfg *menc.ChainConfi if suite.GenesisModify != nil { genesisState = suite.GenesisModify(genesisState, testApp) } + if consensusLabel != "" && chainId == mcfg.MYTHOS_CHAIN_ID_TEST { + testApp.AppCodec().MustUnmarshalJSON(genesisState[wasmxtypes.ModuleName], &wasmxGenState) + var found bool + for i := range wasmxGenState.SystemContracts { + role := wasmxGenState.SystemContracts[i].Role + if role == nil || role.Role != wasmxtypes.ROLE_CONSENSUS { + continue + } + if role.Label == consensusLabel { + role.Primary = true + found = true + } else { + role.Primary = false + } + } + require.Truef(t, found, "consensus-label not found in system contracts: %s", consensusLabel) + feeCollectorBech32, err := addrCodec.BytesToString(authtypes.NewModuleAddress(wasmxtypes.FEE_COLLECTOR)) + require.NoError(t, err) + wasmxGenState.SystemContracts, err = wasmxtypes.FillRoles(wasmxGenState.SystemContracts, addrCodec, feeCollectorBech32) + require.NoError(t, err) + genesisState[wasmxtypes.ModuleName] = testApp.AppCodec().MustMarshalJSON(&wasmxGenState) + } testApp, resInit := ibctesting.InitAppChain(t, testApp, genesisState, chainId) @@ -486,14 +531,17 @@ func (chain TestChain) InitConsensusContract(resInit *abci.ResponseInitChain, no if err != nil { return err } - msg := []byte(`{"run":{"event": {"type": "start", "params": []}}}`) - appA := chain.suite.GetAppContext(&chain) - _, err = chain.GetApp().NetworkKeeper.ExecuteContractInternal(appA.Context(), &types.MsgExecuteContract{ - Sender: wasmxtypes.ROLE_CONSENSUS, - Contract: wasmxtypes.ROLE_CONSENSUS, - Msg: msg, - }) - chain.raftToLeader() + env := chain.suite.StartNodeEnv + if env == nil { + env = map[string]string{} + } + err = networkserver.StartNode(chain.GetApp(), chain.GetApp().Logger(), chain.GetApp().GetNetworkKeeper(), env) + currentState = chain.GetCurrentState(chain.GetContext()) + if strings.Contains(currentState, "Kayros-P2P") { + chain.kayrosToValidator() + } else { + chain.raftToLeader() + } return err } @@ -827,6 +875,63 @@ func (chain TestChain) raftToLeader() { } } +// kayrosToValidator transitions Kayros P2P consensus to Validator state +// Kayros P2P doesn't have leader election - all validators produce blocks based on Kayros ordering +func (chain TestChain) kayrosToValidator() { + currentState := chain.GetCurrentState(chain.GetContext()) + // Check if we're in Kayros P2P consensus + if !strings.Contains(currentState, "Kayros-P2P") { + return + } + + // If in Node state, send becomeValidator event to transition to Validator + if strings.Contains(currentState, "Node") && !strings.Contains(currentState, "Validator") { + msg := []byte(`{"run":{"event": {"type": "becomeValidator", "params": []}}}`) + _, err := chain.App.NetworkKeeper.ExecuteContractInternal(chain.GetContext(), &types.MsgExecuteContract{ + Sender: wasmxtypes.ROLE_CONSENSUS, + Contract: wasmxtypes.ROLE_CONSENSUS, + Msg: msg, + }) + chain.suite.Require().NoError(err) + } + + currentState = chain.GetCurrentState(chain.GetContext()) + chain.suite.Require().Contains(currentState, "Validator") +} + +// KayrosCommitBlock triggers block production for Kayros P2P consensus +// This simulates the Kayros ordering service providing transaction ordering +func (chain TestChain) KayrosCommitBlock() (*abci.ResponseFinalizeBlock, error) { + currentState := chain.GetCurrentState(chain.GetContext()) + if !strings.Contains(currentState, "Kayros-P2P") { + return nil, fmt.Errorf("not in Kayros P2P consensus mode: %s", currentState) + } + + // If in active state, trigger the always transition to propose (if ifAllTransactions guard passes) + // The FSM will call getKayrosTxs, then ifAllTransactions, then commitBlock and sendCommit + if strings.Contains(currentState, "active") { + lastInterval := chain.GetLastInterval(chain.GetContext()) + blockDelay := chain.GetBlockDelay(chain.GetContext()) + + msg1 := []byte(fmt.Sprintf(`{"delay":"%s","state":"%s","intervalId":%s}`, blockDelay, currentState, lastInterval)) + _, err := chain.App.NetworkKeeper.ExecuteEntryPointInternal(chain.GetContext(), wasmxtypes.ENTRY_POINT_TIMED, &types.MsgExecuteContract{ + Sender: wasmxtypes.ROLE_CONSENSUS, + Contract: wasmxtypes.ROLE_CONSENSUS, + Msg: msg1, + }) + if err != nil { + return nil, fmt.Errorf("kayros commit block failed: %v", err) + } + } + + lastBlock := chain.App.LastBlockHeight() + res, _, _, err := chain.GetBlock(chain.GetContext(), lastBlock) + if err != nil { + return nil, fmt.Errorf("get new committed block failed: %v", err) + } + return res, nil +} + func (suite *KeeperTestSuite) commitBlock(chain *TestChain, res *abci.ResponseFinalizeBlock) { _, err := chain.App.Commit() require.NoError(suite.T(), err) diff --git a/wasmx/x/network/server/config/config.go b/wasmx/x/network/server/config/config.go index 1e0e4163..216258a4 100644 --- a/wasmx/x/network/server/config/config.go +++ b/wasmx/x/network/server/config/config.go @@ -1,5 +1,7 @@ package config +import "strings" + const ( DefaultNetworkEnable = true DefaultNetworkLeader = false @@ -27,6 +29,7 @@ type NetworkConfig struct { // comma separated list of values for each initialized chain Id string `mapstructure:"id"` InitialChains []string `mapstructure:"initial-chains"` + StartEnv string `mapstructure:"start-env"` } // DefaultEVMConfig returns the default EVM configuration @@ -39,6 +42,7 @@ func DefaultNetworkConfigConfig() *NetworkConfig { Ips: DefaultNetworkIps, Id: DefaultNodeId, InitialChains: DefaultInitialChains, + StartEnv: "", } } @@ -46,3 +50,24 @@ func (c NetworkConfig) Validate() error { // TODO return nil } + +func ParseStartEnv(raw string) map[string]string { + env := map[string]string{} + for _, pair := range strings.Split(raw, ";") { + pair = strings.TrimSpace(pair) + if pair == "" { + continue + } + parts := strings.SplitN(pair, ":", 2) + if len(parts) != 2 { + continue + } + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + if key == "" { + continue + } + env[key] = value + } + return env +} diff --git a/wasmx/x/network/server/config/toml.go b/wasmx/x/network/server/config/toml.go index 2bbde236..333739a4 100644 --- a/wasmx/x/network/server/config/toml.go +++ b/wasmx/x/network/server/config/toml.go @@ -28,4 +28,8 @@ id = "{{ .Network.Id }}" # Comma separated list of types of chains. E.g. "mythos" or "mythos,level0" initial-chains = [{{ range .Network.InitialChains }}{{ printf "%q, " . }}{{end}}] +# Start node environment variables passed to consensus hooks. +# Format: "kayros_user_key:;kayros_base_url:" +start-env = "{{ .Network.StartEnv }}" + ` diff --git a/wasmx/x/network/server/utils.go b/wasmx/x/network/server/utils.go index c570358f..40ab0687 100644 --- a/wasmx/x/network/server/utils.go +++ b/wasmx/x/network/server/utils.go @@ -5,6 +5,8 @@ import ( "encoding/base64" "encoding/json" "fmt" + "os" + "strings" "cosmossdk.io/log" @@ -28,6 +30,24 @@ import ( wasmxtypes "github.com/loredanacirstea/wasmx/x/wasmx/types" ) +const StartNodeEnvPrefix = "WASMX_STARTNODE_ENV_" + +func ReadStartNodeEnv() map[string]string { + env := make(map[string]string) + for _, entry := range os.Environ() { + key, value, ok := strings.Cut(entry, "=") + if !ok || !strings.HasPrefix(key, StartNodeEnvPrefix) { + continue + } + name := strings.ToLower(strings.TrimPrefix(key, StartNodeEnvPrefix)) + if name == "" { + continue + } + env[name] = value + } + return env +} + func InitChainAndCommitBlock( app servertypes.Application, req *abci.RequestInitChain, @@ -130,10 +150,22 @@ func InitConsensusContract( return nil } -func StartNode(mythosapp mcfg.MythosApp, logger log.Logger, networkServer mcfg.NetworkKeeper) error { +func StartNode(mythosapp mcfg.MythosApp, logger log.Logger, networkServer mcfg.NetworkKeeper, env map[string]string) error { cb := func(goctx context.Context) (any, error) { ctx := sdk.UnwrapSDKContext(goctx) - msg := []byte(fmt.Sprintf(`{"RunHook":{"hook":"%s","data":""}}`, wasmxtypes.HOOK_START_NODE)) + + // Encode env variables as JSON data + envData := "" + if env != nil && len(env) > 0 { + envBz, err := json.Marshal(env) + if err != nil { + return nil, fmt.Errorf("failed to marshal env: %w", err) + } + envData = base64.StdEncoding.EncodeToString(envBz) + logger.Info("StartNode: passing env variables", "count", len(env)) + } + + msg := []byte(fmt.Sprintf(`{"RunHook":{"hook":"%s","data":"%s"}}`, wasmxtypes.HOOK_START_NODE, envData)) res, err := networkServer.ExecuteContractInternal(ctx, &types.MsgExecuteContract{ Sender: wasmxtypes.ROLE_HOOKS_NONC, Contract: wasmxtypes.ROLE_HOOKS_NONC, diff --git a/wasmx/x/network/vmmc/init_app.go b/wasmx/x/network/vmmc/init_app.go index 96620b39..8772e300 100644 --- a/wasmx/x/network/vmmc/init_app.go +++ b/wasmx/x/network/vmmc/init_app.go @@ -132,7 +132,11 @@ func StartApp(ctx *Context, req *StartSubChainMsg) error { if err != nil { return err } - err = networkserver.StartNode(app, logger, app.GetNetworkKeeper()) + + // Read environment variables to pass to consensus + env := networkserver.ReadStartNodeEnv() + + err = networkserver.StartNode(app, logger, app.GetNetworkKeeper(), env) if err != nil { return err } diff --git a/wasmx/x/network/vmp2p/tnd_state_store.go b/wasmx/x/network/vmp2p/tnd_state_store.go index 45caf91f..2c626ced 100644 --- a/wasmx/x/network/vmp2p/tnd_state_store.go +++ b/wasmx/x/network/vmp2p/tnd_state_store.go @@ -270,7 +270,10 @@ func (s StateStore) Bootstrap(state sm.State) error { // TODO send a message to provider to stop state sync - err = networkserver.StartNode(app, logger, app.GetNetworkKeeper()) + // Read environment variables to pass to consensus + env := networkserver.ReadStartNodeEnv() + + err = networkserver.StartNode(app, logger, app.GetNetworkKeeper(), env) if err != nil { return err } diff --git a/wasmx/x/wasmx/keeper/precompiles.go b/wasmx/x/wasmx/keeper/precompiles.go index 4f5b35d2..e3edd09b 100644 --- a/wasmx/x/wasmx/keeper/precompiles.go +++ b/wasmx/x/wasmx/keeper/precompiles.go @@ -155,6 +155,8 @@ func (k *Keeper) ActivateSystemContract( var err error k.SetSystemContract(ctx, contract) + k.Logger(ctx).Info("activating system contract", "label", contract.Label, "hex_address", contract.Address, "code_id", codeID, "role", contract.Role) + if contract.Pinned { if err := k.pinCodeWithEvent(ctx, codeID, codeInfo.CodeHash, compiledFolderPath, contract.MeteringOff); err != nil { return sdkerr.Wrap(err, "pin system contract: "+contract.Label) diff --git a/wasmx/x/wasmx/types/kayros_verifier_schema.json b/wasmx/x/wasmx/types/kayros_verifier_schema.json new file mode 100644 index 00000000..5ea23767 --- /dev/null +++ b/wasmx/x/wasmx/types/kayros_verifier_schema.json @@ -0,0 +1,318 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "wasmx-kayros-verifier", + "description": "JSON Schema for wasmx-kayros-verifier TinyGo contract", + "type": "object", + "oneOf": [ + { + "type": "object", + "properties": { + "verify_proof": { + "$ref": "#/definitions/VerifyProofRequest" + } + }, + "required": [ + "verify_proof" + ] + }, + { + "type": "object", + "properties": { + "verify_proof_hash": { + "$ref": "#/definitions/VerifyProofHashRequest" + } + }, + "required": [ + "verify_proof_hash" + ] + }, + { + "type": "object", + "properties": { + "verify_record_hash": { + "$ref": "#/definitions/VerifyRecordHashRequest" + } + }, + "required": [ + "verify_record_hash" + ] + }, + { + "type": "object", + "properties": { + "verify_record_chain_link": { + "$ref": "#/definitions/VerifyRecordChainLinkRequest" + } + }, + "required": [ + "verify_record_chain_link" + ] + }, + { + "type": "object", + "properties": { + "verify_record_timestamp": { + "$ref": "#/definitions/VerifyRecordTimestampRequest" + } + }, + "required": [ + "verify_record_timestamp" + ] + }, + { + "type": "object", + "properties": { + "verify_record_uuid": { + "$ref": "#/definitions/VerifyRecordUUIDRequest" + } + }, + "required": [ + "verify_record_uuid" + ] + }, + { + "type": "object", + "properties": { + "verify_level_proof": { + "$ref": "#/definitions/VerifyLevelProofRequest" + } + }, + "required": [ + "verify_level_proof" + ] + }, + { + "type": "object", + "properties": { + "verify_proof_path": { + "$ref": "#/definitions/VerifyProofPathRequest" + } + }, + "required": [ + "verify_proof_path" + ] + }, + { + "type": "object", + "properties": { + "verify_kayros_record": { + "$ref": "#/definitions/VerifyKayrosRecordRequest" + } + }, + "required": [ + "verify_kayros_record" + ] + }, + { + "type": "object", + "properties": { + "verify_kayros_record_with_proof": { + "$ref": "#/definitions/VerifyKayrosRecordWithProofRequest" + } + }, + "required": [ + "verify_kayros_record_with_proof" + ] + } + ], + "definitions": { + "VerifyProofRequest": { + "type": "object", + "properties": { + "data_base64": { + "type": "string" + }, + "data_type": { + "type": "string" + }, + "hash_algo": { + "type": "string" + }, + "api_base_url": { + "type": "string" + }, + "api_user_key": { + "type": "string" + } + } + }, + "VerifyProofHashRequest": { + "type": "object", + "properties": { + "data_type": { + "type": "string" + }, + "data_hash": { + "type": "string" + }, + "api_base_url": { + "type": "string" + }, + "api_user_key": { + "type": "string" + } + } + }, + "VerifyRecordHashRequest": { + "type": "object", + "properties": { + "record": { + "$ref": "#/definitions/KayrosRecord" + }, + "hash_algo": { + "type": "string" + } + } + }, + "VerifyRecordChainLinkRequest": { + "type": "object", + "properties": { + "record": { + "$ref": "#/definitions/KayrosRecord" + }, + "prev": { + "$ref": "#/definitions/KayrosRecord" + } + } + }, + "VerifyRecordTimestampRequest": { + "type": "object", + "properties": { + "record": { + "$ref": "#/definitions/KayrosRecord" + } + } + }, + "VerifyRecordUUIDRequest": { + "type": "object", + "properties": { + "record": { + "$ref": "#/definitions/KayrosRecord" + } + } + }, + "VerifyLevelProofRequest": { + "type": "object", + "properties": { + "proof": { + "$ref": "#/definitions/LevelProof" + }, + "hash_algo": { + "type": "string" + }, + "api_base_url": { + "type": "string" + }, + "api_user_key": { + "type": "string" + } + } + }, + "VerifyProofPathRequest": { + "type": "object", + "properties": { + "proof": { + "$ref": "#/definitions/ProofPathData" + }, + "hash_algo": { + "type": "string" + } + } + }, + "VerifyKayrosRecordRequest": { + "type": "object", + "properties": { + "record": { + "$ref": "#/definitions/KayrosRecord" + }, + "prev": { + "$ref": "#/definitions/KayrosRecord" + }, + "hash_algo": { + "type": "string" + }, + "level_proofs": { + "type": "array", + "items": { + "$ref": "#/definitions/LevelProof" + } + }, + "api_base_url": { + "type": "string" + }, + "api_user_key": { + "type": "string" + } + } + }, + "VerifyKayrosRecordWithProofRequest": { + "type": "object", + "properties": { + "record": { + "$ref": "#/definitions/KayrosRecord" + }, + "prev": { + "$ref": "#/definitions/KayrosRecord" + }, + "proof": { + "$ref": "#/definitions/ProofPathData" + }, + "hash_algo": { + "type": "string" + } + } + }, + "KayrosRecord": { + "type": "object", + "properties": { + "data_type": { "type": "string" }, + "data_type_hex": { "type": "string" }, + "data_item_hex": { "type": "string" }, + "uuid_hex": { "type": "string" }, + "hash_item_hex": { "type": "string" }, + "prev_hash_hex": { "type": "string" }, + "hash_type": { "type": "string" }, + "timestamp": { "type": "string" } + } + }, + "LevelProof": { + "type": "object", + "properties": { + "level": { "type": "integer" }, + "position": { "type": "integer" }, + "hashes": { + "type": "array", + "items": { "type": "string" } + } + } + }, + "ProofPathData": { + "type": "object", + "properties": { + "hash_item_hex": { "type": "string" }, + "data_type_hex": { "type": "string" }, + "data_item_hex": { "type": "string" }, + "record_index": { "type": "integer" }, + "proof": { + "type": "array", + "items": { "$ref": "#/definitions/ProofPathEntry" } + }, + "root_hash_hex": { "type": "string" } + } + }, + "ProofPathEntry": { + "type": "object", + "properties": { + "level": { "type": "integer" }, + "position": { "type": "integer" }, + "hash_hex": { "type": "string" }, + "index_in_block": { "type": "integer" }, + "sibling_hashes": { + "type": "array", + "items": { "type": "string" } + }, + "sibling_count": { "type": "integer" } + } + } + } +} diff --git a/wasmx/x/wasmx/types/system_contract.go b/wasmx/x/wasmx/types/system_contract.go index d50cae65..397bf51a 100644 --- a/wasmx/x/wasmx/types/system_contract.go +++ b/wasmx/x/wasmx/types/system_contract.go @@ -2,6 +2,7 @@ package types import ( bytes "bytes" + _ "embed" "encoding/json" "fmt" "slices" @@ -83,8 +84,18 @@ var ADDR_EMAIL_HANDLER = "0x0000000000000000000000000000000000000063" var ADDR_ONDEMAND_SINGLE_LIBRARY = "0x0000000000000000000000000000000000000064" var ADDR_ONDEMAND_SINGLE = "0x0000000000000000000000000000000000000065" +var ADDR_CONSENSUS_KAYROSP2P_LIBRARY = "0x0000000000000000000000000000000000000071" +var ADDR_CONSENSUS_KAYROSP2P = "0x0000000000000000000000000000000000000072" + +var ADDR_CONSENSUS_KAYROSP2P_ONDEMAND_LIBRARY = "0x0000000000000000000000000000000000000073" +var ADDR_CONSENSUS_KAYROSP2P_ONDEMAND = "0x0000000000000000000000000000000000000074" +var ADDR_KAYROS_VERIFIER = "0x0000000000000000000000000000000000000075" + var ADDR_SYS_PROXY = "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" +//go:embed kayros_verifier_schema.json +var kayrosVerifierSchema string + func StarterPrecompiles() SystemContracts { msg := WasmxExecutionMessage{Data: []byte{}} initMsg, err := json.Marshal(msg) @@ -598,6 +609,19 @@ func ConsensusPrecompiles(minValidatorCount int32, enableEIDCheck bool, currentL panic("ConsensusPrecompiles: cannot marshal level0OnDemandInitMsg message") } + // genesis uuid is a valid time-based uuid that should not exist in the kayros database - for a new chain + kayrosP2PInitMsg, err := json.Marshal(WasmxExecutionMessage{Data: []byte(`{"instantiate":{"context":[{"key":"blockTimeout","value":"timeoutCommit"},{"key":"max_tx_bytes","value":"65536"},{"key":"timeoutCommit","value":"4000"},{"key":"max_block_gas","value":"20000000"},{"key":"timeoutMissingTxs","value":"4000"},{"key":"kayros_base_url","value":""},{"key":"kayros_user_key","value":""},{"key":"threshold_commit","value":"51"},{"key":"threshold_finalize","value":"75"},{"key":"genesis_uuid","value":""},{"key":"data_type_id","value":""},{"key":"max_block_tx","value":"30"}],"initialState":"uninitialized"}}`)}) + if err != nil { + panic("ConsensusPrecompiles: cannot marshal kayrosP2PInitMsg message") + } + + // genesis uuid is a valid time-based uuid that should not exist in the kayros database - for a new chain + kayrosP2POnDemandInitMsg, err := json.Marshal(WasmxExecutionMessage{Data: []byte(`{"instantiate":{"context":[{"key":"blockTimeout","value":"timeoutCommit"},{"key":"max_tx_bytes","value":"65536"},{"key":"timeoutCommit","value":"100"},{"key":"max_block_gas","value":"20000000"},{"key":"timeoutMissingTxs","value":"4000"},{"key":"kayros_base_url","value":""},{"key":"kayros_user_key","value":""},{"key":"threshold_commit","value":"51"},{"key":"threshold_finalize","value":"75"},{"key":"genesis_uuid","value":""},{"key":"data_type_id","value":""},{"key":"max_block_tx","value":"30"},{"key":"batchTimeout","value":"1000"}],"initialState":"uninitialized"}}`)}) + + if err != nil { + panic("ConsensusPrecompiles: cannot marshal kayrosP2PInitMsg message") + } + return []SystemContract{ { Address: ADDR_CONSENSUS_RAFT_LIBRARY, @@ -800,6 +824,44 @@ func ConsensusPrecompiles(minValidatorCount int32, enableEIDCheck bool, currentL StorageType: ContractStorageType_SingleConsensus, Deps: []string{INTERPRETER_FSM, BuildDep(ADDR_LEVEL0_ONDEMAND_LIBRARY, ROLE_LIBRARY)}, }, + { + Address: ADDR_CONSENSUS_KAYROSP2P_LIBRARY, + Label: CONSENSUS_KAYROSP2P_LIBRARY, + InitMessage: initMsg, + Pinned: true, + MeteringOff: true, + Role: &SystemContractRole{Role: ROLE_LIBRARY, Label: CONSENSUS_KAYROSP2P_LIBRARY}, + StorageType: ContractStorageType_SingleConsensus, + Deps: []string{}, + }, + { + Address: ADDR_CONSENSUS_KAYROSP2P, + Label: CONSENSUS_KAYROSP2P, + InitMessage: kayrosP2PInitMsg, + Pinned: false, + Role: &SystemContractRole{Role: ROLE_CONSENSUS, Label: CONSENSUS_KAYROSP2P}, + StorageType: ContractStorageType_SingleConsensus, + Deps: []string{INTERPRETER_FSM, BuildDep(ADDR_CONSENSUS_KAYROSP2P_LIBRARY, ROLE_LIBRARY)}, + }, + { + Address: ADDR_CONSENSUS_KAYROSP2P_ONDEMAND_LIBRARY, + Label: CONSENSUS_KAYROSP2P_ONDEMAND_LIBRARY, + InitMessage: initMsg, + Pinned: true, + MeteringOff: true, + Role: &SystemContractRole{Role: ROLE_LIBRARY, Label: CONSENSUS_KAYROSP2P_ONDEMAND_LIBRARY}, + StorageType: ContractStorageType_SingleConsensus, + Deps: []string{}, + }, + { + Address: ADDR_CONSENSUS_KAYROSP2P_ONDEMAND, + Label: CONSENSUS_KAYROSP2P_ONDEMAND, + InitMessage: kayrosP2POnDemandInitMsg, + Pinned: false, + Role: &SystemContractRole{Role: ROLE_CONSENSUS, Label: CONSENSUS_KAYROSP2P_ONDEMAND}, + StorageType: ContractStorageType_SingleConsensus, + Deps: []string{INTERPRETER_FSM, BuildDep(ADDR_CONSENSUS_KAYROSP2P_ONDEMAND_LIBRARY, ROLE_LIBRARY)}, + }, } } @@ -852,6 +914,27 @@ func ChatPrecompiles() SystemContracts { } } +func VerifierPrecompiles() SystemContracts { + msg := WasmxExecutionMessage{Data: []byte{}} + initMsg, err := json.Marshal(msg) + if err != nil { + panic("VerifierPrecompiles: cannot marshal init message") + } + return []SystemContract{ + { + Address: ADDR_KAYROS_VERIFIER, + Label: KAYROS_VERIFIER_v001, + InitMessage: initMsg, + Pinned: true, + MeteringOff: true, + Role: &SystemContractRole{Role: ROLE_VERIFIER, Label: KAYROS_VERIFIER_v001, Primary: true}, + StorageType: ContractStorageType_CoreConsensus, + Deps: []string{}, + Metadata: CodeMetadataPB{JsonSchema: kayrosVerifierSchema}, + }, + } +} + func SpecialPrecompiles() SystemContracts { msg := WasmxExecutionMessage{Data: []byte{}} initMsg, err := json.Marshal(msg) @@ -918,6 +1001,7 @@ func DefaultSystemContracts(accBech32Codec mcodec.AccBech32Codec, feeCollectorBe precompiles = append(precompiles, consensusPrecompiles...) precompiles = append(precompiles, MultiChainPrecompiles(minValidatorCount, enableEIDCheck, erc20CodeId, derc20CodeId)...) precompiles = append(precompiles, ChatPrecompiles()...) + precompiles = append(precompiles, VerifierPrecompiles()...) // precompiles = append(precompiles, SpecialPrecompiles()...) precompiles, err := FillRoles(precompiles, accBech32Codec, feeCollectorBech32) @@ -1019,6 +1103,7 @@ func DefaultTimeChainContracts(accBech32Codec mcodec.AccBech32Codec, feeCollecto precompiles = append(precompiles, consensusPrecompiles...) precompiles = append(precompiles, MultiChainPrecompiles(minValidatorCount, enableEIDCheck, erc20CodeId, derc20CodeId)...) precompiles = append(precompiles, ChatPrecompiles()...) + precompiles = append(precompiles, VerifierPrecompiles()...) // precompiles = append(precompiles, SpecialPrecompiles()...) precompiles, err = FillRoles(precompiles, accBech32Codec, feeCollectorBech32) @@ -1028,50 +1113,6 @@ func DefaultTimeChainContracts(accBech32Codec mcodec.AccBech32Codec, feeCollecto return precompiles } -func DefaultOnDemandSingleContracts(accBech32Codec mcodec.AccBech32Codec, feeCollectorBech32 string, mintBech32 string, minValidatorCount int32, enableEIDCheck bool, initialPortValues string, bondBaseDenom string) SystemContracts { - - precompiles := StarterPrecompiles() - precompiles = append(precompiles, SimplePrecompiles()...) - precompiles = append(precompiles, InterpreterPrecompiles()...) - precompiles = append(precompiles, BasePrecompiles()...) - precompiles = append(precompiles, EIDPrecompiles()...) - precompiles = append(precompiles, HookPrecompiles()...) - precompiles = append(precompiles, CosmosPrecompiles(feeCollectorBech32, mintBech32, bondBaseDenom)...) - - erc20CodeId := int32(0) - derc20CodeId := int32(0) - for i, p := range precompiles { - if p.Label == ERC20_v001 { - erc20CodeId = int32(i + 1) - } - } - for i, p := range precompiles { - if p.Label == DERC20_v001 { - derc20CodeId = int32(i + 1) - } - } - if erc20CodeId == int32(0) || derc20CodeId == int32(0) { - panic(fmt.Sprintf("erc20 or derc20 contracts not found: erc20CodeId %d, derc20CodeId %d", erc20CodeId, derc20CodeId)) - } - - consensusPrecompiles := ConsensusPrecompiles(minValidatorCount, enableEIDCheck, 0, initialPortValues, erc20CodeId, derc20CodeId) - for i, val := range consensusPrecompiles { - if val.Label == ONDEMAND_SINGLE_v001 { - consensusPrecompiles[i].Role.Primary = true - } - } - precompiles = append(precompiles, consensusPrecompiles...) - precompiles = append(precompiles, MultiChainPrecompiles(minValidatorCount, enableEIDCheck, erc20CodeId, derc20CodeId)...) - precompiles = append(precompiles, ChatPrecompiles()...) - // precompiles = append(precompiles, SpecialPrecompiles()...) - - precompiles, err := FillRoles(precompiles, accBech32Codec, feeCollectorBech32) - if err != nil { - panic(err) - } - return precompiles -} - func (p SystemContract) Validate() error { if err := validateString(p.Label); err != nil { return err @@ -1227,6 +1268,10 @@ func FillRoles(precompiles []SystemContract, accBech32Codec mcodec.AccBech32Code } } + if entry, exists := roleMap[ROLE_VERIFIER]; exists { + entry.Multiple = true + } + // add denom role if _, exists := roleMap[ROLE_DENOM]; !exists { order = append(order, ROLE_DENOM) diff --git a/wasmx/x/wasmx/types/vm.go b/wasmx/x/wasmx/types/vm.go index 715f3a6b..fff96143 100644 --- a/wasmx/x/wasmx/types/vm.go +++ b/wasmx/x/wasmx/types/vm.go @@ -281,6 +281,7 @@ var ROLE_CHAT = "chat" var ROLE_TIME = "time" var ROLE_LEVEL0 = "level0" var ROLE_LEVEL0_ON_DEMAND = "level0_ondemand" +var ROLE_VERIFIER = "verifier" var ROLE_MULTICHAIN_REGISTRY = "multichain_registry" var ROLE_MULTICHAIN_REGISTRY_LOCAL = "multichain_registry_local" @@ -331,6 +332,11 @@ var CONSENSUS_TENDERMINTP2P_LIBRARY = "tendermintp2p_library" var CONSENSUS_AVA_SNOWMAN = "consensus_ava_snowman_0.0.1" var CONSENSUS_AVA_SNOWMAN_LIBRARY = "ava_snowman_library" +var CONSENSUS_KAYROSP2P = "consensus_kayrosp2p_0.0.1" +var CONSENSUS_KAYROSP2P_LIBRARY = "kayrosp2p_library" +var CONSENSUS_KAYROSP2P_ONDEMAND = "consensus_kayrosp2p_ondemand_0.0.1" +var CONSENSUS_KAYROSP2P_ONDEMAND_LIBRARY = "kayrosp2p_ondemand_library" + var CONSENSUS_LEVEL_LIBRARY = "level0_library" var LEVEL0_v001 = "level0_0.0.1" var LEVEL0_ONDEMAND_v001 = "level0_ondemand_0.0.1" @@ -362,6 +368,7 @@ var SLASHING_v001 = "slashing_0.0.1" var DISTRIBUTION_v001 = "distribution_0.0.1" var CHAT_v001 = "chat_0.0.1" var CHAT_VERIFIER_v001 = "chat_verifier_0.0.1" +var KAYROS_VERIFIER_v001 = "kayros_verifier_0.0.1" var TIME_v001 = "time_0.0.1" var MULTICHAIN_REGISTRY_v001 = "multichain_registry_0.0.1" var MULTICHAIN_REGISTRY_LOCAL_v001 = "multichain_registry_local_0.0.1" diff --git a/wasmx/x/wasmx/vm/precompiles/28.finite_state_machine.wasm b/wasmx/x/wasmx/vm/precompiles/28.finite_state_machine.wasm index 445a9181..eb3158a8 100644 Binary files a/wasmx/x/wasmx/vm/precompiles/28.finite_state_machine.wasm and b/wasmx/x/wasmx/vm/precompiles/28.finite_state_machine.wasm differ diff --git a/wasmx/x/wasmx/vm/precompiles/71.kayrosp2p_library.wasm b/wasmx/x/wasmx/vm/precompiles/71.kayrosp2p_library.wasm new file mode 100644 index 00000000..b56f2924 Binary files /dev/null and b/wasmx/x/wasmx/vm/precompiles/71.kayrosp2p_library.wasm differ diff --git a/wasmx/x/wasmx/vm/precompiles/72.kayrosp2p_ondemand_library.wasm b/wasmx/x/wasmx/vm/precompiles/72.kayrosp2p_ondemand_library.wasm new file mode 100644 index 00000000..09f91bd8 Binary files /dev/null and b/wasmx/x/wasmx/vm/precompiles/72.kayrosp2p_ondemand_library.wasm differ diff --git a/wasmx/x/wasmx/vm/precompiles/75.kayros_verifier_0.0.1.wasm b/wasmx/x/wasmx/vm/precompiles/75.kayros_verifier_0.0.1.wasm new file mode 100644 index 00000000..b2716d86 Binary files /dev/null and b/wasmx/x/wasmx/vm/precompiles/75.kayros_verifier_0.0.1.wasm differ diff --git a/wasmx/x/wasmx/vm/precompiles/fsm.go b/wasmx/x/wasmx/vm/precompiles/fsm.go index 6f8b2076..e6fd210d 100644 --- a/wasmx/x/wasmx/vm/precompiles/fsm.go +++ b/wasmx/x/wasmx/vm/precompiles/fsm.go @@ -39,3 +39,11 @@ var Level0OnDemand001 = func(libAddressBech32 string) string { var OnDemandSingle = func(libAddressBech32 string) string { return fmt.Sprintf(`{"library":"%s","id":"Levels-OnDemand-Single","initial":"uninitialized","states":[{"name":"uninitialized","after":[],"always":[],"on":[{"name":"initialize","transitions":[{"target":"#OnDemand-Single.initialized","guard":null,"actions":[],"meta":[]}]}],"entry":[],"exit":[],"initial":"","states":[]},{"name":"initialized","after":[],"always":[],"on":[],"entry":[],"exit":[],"initial":"unstarted","states":[{"name":"unstarted","after":[],"always":[],"on":[{"name":"start","transitions":[{"target":"#OnDemand-Single.initialized.started","guard":null,"actions":[{"type":"StartNode","params":[]}],"meta":[]}]},{"name":"setup","transitions":[{"target":"#OnDemand-Single.initialized.unstarted","guard":null,"actions":[{"type":"setup","params":[]}],"meta":[]}]},{"name":"prestart","transitions":[{"target":"#OnDemand-Single.initialized.prestart","guard":null,"actions":[],"meta":[]}]},{"name":"setupNode","transitions":[{"target":"#OnDemand-Single.initialized.unstarted","guard":null,"actions":[{"type":"setupNode","params":[]}],"meta":[]}]}],"entry":[],"exit":[],"initial":"","states":[]},{"name":"started","after":[],"always":[],"on":[{"name":"start","transitions":[{"target":"#OnDemand-Single.initialized.started","guard":null,"actions":[{"type":"StartNode","params":[]}],"meta":[]}]}],"entry":[],"exit":[],"initial":"active","states":[{"name":"active","after":[],"always":[],"on":[{"name":"newTransaction","transitions":[{"target":"#OnDemand-Single.initialized.started.proposer","guard":{"type":"ifNewTransaction","params":[]},"actions":[{"type":"addToMempool","params":[]},{"type":"sendNewTransactionResponse","params":[]}],"meta":[]}]}],"entry":[],"exit":[],"initial":"","states":[]},{"name":"proposer","after":[],"always":[{"name":"always","transitions":[{"target":"#OnDemand-Single.initialized.started.active","actions":[{"type":"newBlock","params":[]}],"guard":null,"meta":[]}]}],"on":[],"entry":[],"exit":[],"initial":"","states":[]}]},{"name":"prestart","after":[{"name":"500","transitions":[{"target":"#OnDemand-Single.initialized.started","guard":null,"actions":[],"meta":[]}]}],"always":[],"on":[],"entry":[],"exit":[],"initial":"","states":[]}]}]}`, libAddressBech32) } + +var KayrosP2P = func(libAddressBech32 string) string { + return fmt.Sprintf(`{"library":"%s","id":"Kayros-P2P-1","initial":"uninitialized","states":[{"name":"uninitialized","after":[],"always":[],"on":[{"name":"initialize","transitions":[{"target":"#Kayros-P2P-1.initialized","guard":null,"actions":[],"meta":[]}]}],"entry":[],"exit":[],"initial":"","states":[]},{"name":"initialized","after":[],"always":[],"on":[],"entry":[],"exit":[],"initial":"unstarted","states":[{"name":"unstarted","after":[],"always":[],"on":[{"name":"setupNode","transitions":[{"target":"#Kayros-P2P-1.initialized.unstarted","guard":null,"actions":[{"type":"setupNode","params":[]}],"meta":[]}]},{"name":"prestart","transitions":[{"target":"#Kayros-P2P-1.initialized.prestart","guard":null,"actions":[],"meta":[]}]},{"name":"setup","transitions":[{"target":"#Kayros-P2P-1.initialized.unstarted","guard":null,"actions":[{"type":"setup","params":[]}],"meta":[]}]},{"name":"start","transitions":[{"target":"#Kayros-P2P-1.initialized.started","guard":null,"actions":[{"type":"connectPeers","params":[]},{"type":"connectRooms","params":[]},{"type":"requestBlockSync","params":[]}],"meta":[]}]}],"entry":[],"exit":[],"initial":"","states":[]},{"name":"prestart","after":[{"name":"500","transitions":[{"target":"#Kayros-P2P-1.initialized.started","guard":null,"actions":[],"meta":[]}]}],"always":[],"on":[],"entry":[],"exit":[],"initial":"","states":[]},{"name":"started","after":[],"always":[],"on":[{"name":"newTransaction","transitions":[{"target":"","guard":{"type":"ifNewTransaction","params":[]},"actions":[{"type":"addToMempool","params":[]},{"type":"registerWithKayros","params":[]},{"type":"sendNewTransactionResponse","params":[]},{"type":"forwardMsgToChat","params":[{"key":"protocolId","value":"mempool"}]}],"meta":[]}]},{"name":"receiveStateSyncRequest","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveStateSyncRequest","params":[]}],"meta":[]}]},{"name":"updateNode","transitions":[{"target":"","guard":null,"actions":[{"type":"updateNodeAndReturn","params":[]}],"meta":[]}]}],"entry":[],"exit":[],"initial":"Node","states":[{"name":"Node","after":[],"always":[{"name":"always","transitions":[{"target":"#Kayros-P2P-1.initialized.started.Validator","actions":[{"type":"registerValidatorWithNetwork","params":[]}],"guard":{"type":"ifNodeIsValidator","params":[]},"meta":[]}]}],"on":[{"name":"becomeValidator","transitions":[{"target":"#Kayros-P2P-1.initialized.started.Validator","guard":null,"actions":[{"type":"registerValidatorWithNetwork","params":[]},{"type":"requestBlockSync","params":[]}],"meta":[]}]},{"name":"receiveStateSyncResponse","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveStateSyncResponse","params":[]}],"meta":[]}]},{"name":"start","transitions":[{"target":"","guard":null,"actions":[{"type":"connectPeers","params":[]},{"type":"connectRooms","params":[]},{"type":"requestBlockSync","params":[]}],"meta":[]}]},{"name":"receiveCommit","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveCommit","params":[]}],"meta":[]}]},{"name":"receiveUpdateNodeResponse","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveUpdateNodeResponse","params":[]}],"meta":[]}]}],"entry":[],"exit":[],"initial":"","states":[]},{"name":"Validator","after":[],"always":[],"on":[{"name":"stop","transitions":[{"target":"#Kayros-P2P-1.stopped","guard":null,"actions":[],"meta":[]}]},{"name":"receiveUpdateNodeResponse","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveUpdateNodeResponse","params":[]}],"meta":[]}]},{"name":"start","transitions":[{"target":"#Kayros-P2P-1.initialized.started.Validator","guard":null,"actions":[{"type":"connectPeers","params":[]},{"type":"connectRooms","params":[]},{"type":"registerValidatorWithNetwork","params":[]},{"type":"requestBlockSync","params":[]}],"meta":[]}]},{"name":"receiveCommit","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveCommit","params":[]}],"meta":[]}]},{"name":"receiveUpdateNodeRequest","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveUpdateNodeRequest","params":[]}],"meta":[]}]},{"name":"receiveStateSyncResponse","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveStateSyncResponse","params":[]},{"type":"requestValidatorNodeInfoIfSynced","params":[]}],"meta":[]}]}],"entry":[],"exit":[],"initial":"active","states":[{"name":"active","after":[{"name":"timeoutMissingTxs","transitions":[{"target":"#Kayros-P2P-1.initialized.started.Validator.propose","guard":null,"actions":[],"meta":[]}]}],"always":[{"name":"always","transitions":[{"target":"#Kayros-P2P-1.initialized.started.Validator.propose","guard":{"type":"ifAllTransactions","params":[]},"actions":[],"meta":[]}]}],"on":[{"name":"receiveMissingTransactions","transitions":[{"target":"#Kayros-P2P-1.initialized.started.Validator.active","guard":null,"actions":[{"type":"receiveMissingTransactions","params":[]}],"meta":[]}]}],"entry":[{"type":"getKayrosTxs","params":[]},{"type":"cancelActiveIntervals","params":[{"key":"after","value":"timeoutMissingTxs"}]}],"exit":[],"initial":"","states":[]},{"name":"propose","after":[{"name":"timeoutCommit","transitions":[{"target":"#Kayros-P2P-1.initialized.started.Validator.active","guard":null,"actions":[],"meta":[]}]}],"always":[],"on":[],"entry":[{"type":"commitBlock","params":[]},{"type":"sendCommit","params":[]},{"type":"cancelActiveIntervals","params":[{"key":"after","value":"timeoutCommit"}]}],"exit":[],"initial":"","states":[]}]}]}]},{"name":"stopped","after":[],"always":[],"on":[{"name":"restart","transitions":[{"target":"#Kayros-P2P-1.initialized.unstarted","guard":null,"actions":[],"meta":[]}]}],"entry":[],"exit":[],"initial":"","states":[]}]}`, libAddressBech32) +} + +var KayrosP2POnDemand = func(libAddressBech32 string) string { + return fmt.Sprintf(`{"library":"%s","id":"Kayros-P2P-OnDemand-1","initial":"uninitialized","states":[{"name":"uninitialized","after":[],"always":[],"on":[{"name":"initialize","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized","guard":null,"actions":[],"meta":[]}]}],"entry":[],"exit":[],"initial":"","states":[]},{"name":"initialized","after":[],"always":[],"on":[],"entry":[],"exit":[],"initial":"unstarted","states":[{"name":"unstarted","after":[],"always":[],"on":[{"name":"setupNode","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.unstarted","guard":null,"actions":[{"type":"setupNode","params":[]}],"meta":[]}]},{"name":"prestart","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.prestart","guard":null,"actions":[],"meta":[]}]},{"name":"setup","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.unstarted","guard":null,"actions":[{"type":"setup","params":[]}],"meta":[]}]},{"name":"start","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.started","guard":null,"actions":[{"type":"connectPeers","params":[]},{"type":"connectRooms","params":[]},{"type":"requestBlockSync","params":[]}],"meta":[]}]}],"entry":[],"exit":[],"initial":"","states":[]},{"name":"prestart","after":[{"name":"500","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.started","guard":null,"actions":[],"meta":[]}]}],"always":[],"on":[],"entry":[],"exit":[],"initial":"","states":[]},{"name":"started","after":[],"always":[],"on":[{"name":"newTransaction","transitions":[{"target":"","guard":{"type":"ifNewTransaction","params":[]},"actions":[{"type":"addToMempool","params":[]},{"type":"registerWithKayros","params":[]},{"type":"sendNewTransactionResponse","params":[]},{"type":"forwardMsgToChat","params":[{"key":"protocolId","value":"mempool"}]}],"meta":[]}]},{"name":"receiveStateSyncRequest","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveStateSyncRequest","params":[]}],"meta":[]}]},{"name":"updateNode","transitions":[{"target":"","guard":null,"actions":[{"type":"updateNodeAndReturn","params":[]}],"meta":[]}]}],"entry":[],"exit":[],"initial":"Node","states":[{"name":"Node","after":[],"always":[{"name":"always","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.started.Validator","actions":[{"type":"registerValidatorWithNetwork","params":[]}],"guard":{"type":"ifNodeIsValidator","params":[]},"meta":[]}]}],"on":[{"name":"becomeValidator","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.started.Validator","guard":null,"actions":[{"type":"registerValidatorWithNetwork","params":[]},{"type":"requestBlockSync","params":[]}],"meta":[]}]},{"name":"receiveStateSyncResponse","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveStateSyncResponse","params":[]}],"meta":[]}]},{"name":"start","transitions":[{"target":"","guard":null,"actions":[{"type":"connectPeers","params":[]},{"type":"connectRooms","params":[]},{"type":"requestBlockSync","params":[]}],"meta":[]}]},{"name":"receiveCommit","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveCommit","params":[]}],"meta":[]}]},{"name":"receiveUpdateNodeResponse","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveUpdateNodeResponse","params":[]}],"meta":[]}]}],"entry":[],"exit":[],"initial":"","states":[]},{"name":"Validator","after":[],"always":[],"on":[{"name":"stop","transitions":[{"target":"#Kayros-P2P-OnDemand-1.stopped","guard":null,"actions":[],"meta":[]}]},{"name":"receiveUpdateNodeResponse","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveUpdateNodeResponse","params":[]}],"meta":[]}]},{"name":"start","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.started.Validator","guard":null,"actions":[{"type":"connectPeers","params":[]},{"type":"connectRooms","params":[]},{"type":"registerValidatorWithNetwork","params":[]},{"type":"requestBlockSync","params":[]}],"meta":[]}]},{"name":"receiveCommit","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveCommit","params":[]}],"meta":[]}]},{"name":"receiveUpdateNodeRequest","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveUpdateNodeRequest","params":[]}],"meta":[]}]},{"name":"receiveStateSyncResponse","transitions":[{"target":"","guard":null,"actions":[{"type":"receiveStateSyncResponse","params":[]},{"type":"requestValidatorNodeInfoIfSynced","params":[]}],"meta":[]}]}],"entry":[],"exit":[],"initial":"active","states":[{"name":"active","after":[{"name":"timeoutMissingTxs","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.started.Validator.propose","guard":null,"actions":[],"meta":[]}]}],"always":[{"name":"always","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.started.Validator.propose","guard":{"type":"ifAllTransactions","params":[]},"actions":[],"meta":[]}]}],"on":[{"name":"receiveMissingTransactions","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.started.Validator.active","guard":null,"actions":[{"type":"receiveMissingTransactions","params":[]}],"meta":[]}]}],"entry":[{"type":"getKayrosTxs","params":[]},{"type":"cancelActiveIntervals","params":[{"key":"after","value":"timeoutMissingTxs"}]}],"exit":[],"initial":"","states":[]},{"name":"propose","after":[{"name":"timeoutCommit","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.started.waiting","guard":null,"actions":[],"meta":[]}]}],"always":[],"on":[],"entry":[{"type":"commitBlock","params":[]},{"type":"sendCommit","params":[]},{"type":"cancelActiveIntervals","params":[{"key":"after","value":"timeoutCommit"}]}],"exit":[],"initial":"","states":[]}]},{"name":"waiting","after":[{"name":"batchTimeout","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.started.Validator.active","guard":{"type":"ifMempoolNotEmpty","params":[]},"actions":[],"meta":[]}]}],"always":[],"on":[{"name":"newTransaction","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.started.waiting","guard":{"type":"ifOldTransaction","params":[]},"actions":[],"meta":[]},{"target":"#Kayros-P2P-OnDemand-1.initialized.started.waiting","guard":{"type":"ifMempoolEmpty","params":[]},"actions":[{"type":"addToMempool","params":[]},{"type":"registerWithKayros","params":[]},{"type":"sendNewTransactionResponse","params":[]},{"type":"forwardMsgToChat","params":[{"key":"protocolId","value":"mempool"}]}],"meta":[]},{"target":"#Kayros-P2P-OnDemand-1.initialized.started.waiting","guard":{"type":"ifMempoolFull","params":[]},"actions":[{"type":"addToMempool","params":[]},{"type":"registerWithKayros","params":[]},{"type":"sendNewTransactionResponse","params":[]},{"type":"forwardMsgToChat","params":[{"key":"protocolId","value":"mempool"}]},{"type":"cancelActiveIntervals","params":[{"key":"after","value":"batchTimeout"}]}],"meta":[]},{"target":"#Kayros-P2P-OnDemand-1.initialized.started.waiting","guard":null,"actions":[{"type":"addToMempool","params":[]},{"type":"registerWithKayros","params":[]},{"type":"sendNewTransactionResponse","params":[{"key":"protocolId","value":"mempool"}]},{"type":"forwardMsgToChat","params":[{"key":"protocolId","value":"mempool"}]}],"meta":[]}]}],"entry":[],"exit":[],"initial":"","states":[]}]}]},{"name":"stopped","after":[],"always":[],"on":[{"name":"restart","transitions":[{"target":"#Kayros-P2P-OnDemand-1.initialized.unstarted","guard":null,"actions":[],"meta":[]}]}],"entry":[],"exit":[],"initial":"","states":[]}]}`, libAddressBech32) +} diff --git a/wasmx/x/wasmx/vm/precompiles/precompiles.go b/wasmx/x/wasmx/vm/precompiles/precompiles.go index c24b5083..423d85fe 100644 --- a/wasmx/x/wasmx/vm/precompiles/precompiles.go +++ b/wasmx/x/wasmx/vm/precompiles/precompiles.go @@ -162,6 +162,15 @@ var ( //go:embed 65.wasmx_ondemand_single_library.wasm ondemand_single_library []byte + //go:embed 71.kayrosp2p_library.wasm + kayrosp2p_library []byte + + //go:embed 72.kayrosp2p_ondemand_library.wasm + kayrosp2p_ondemand_library []byte + + //go:embed 75.kayros_verifier_0.0.1.wasm + kayros_verifier_contract []byte + //go:embed ff.sys_proxy.wasm sys_proxy []byte ) @@ -307,6 +316,26 @@ func GetPrecompileByLabel(addrCodec address.Codec, label string) []byte { panic(err) } wasmbin = []byte(OnDemandSingle(libaddrstr)) + case types.CONSENSUS_KAYROSP2P_LIBRARY: + wasmbin = kayrosp2p_library + case types.CONSENSUS_KAYROSP2P: + libaddr := types.AccAddressFromHex(types.ADDR_CONSENSUS_KAYROSP2P_LIBRARY) + libaddrstr, err := addrCodec.BytesToString(libaddr) + if err != nil { + panic(err) + } + wasmbin = []byte(KayrosP2P(libaddrstr)) + case types.CONSENSUS_KAYROSP2P_ONDEMAND_LIBRARY: + wasmbin = kayrosp2p_ondemand_library + case types.CONSENSUS_KAYROSP2P_ONDEMAND: + libaddr := types.AccAddressFromHex(types.ADDR_CONSENSUS_KAYROSP2P_ONDEMAND_LIBRARY) + libaddrstr, err := addrCodec.BytesToString(libaddr) + if err != nil { + panic(err) + } + wasmbin = []byte(KayrosP2POnDemand(libaddrstr)) + case types.KAYROS_VERIFIER_v001: + wasmbin = kayros_verifier_contract case types.MULTICHAIN_REGISTRY_v001: wasmbin = multichain_registry case types.MULTICHAIN_REGISTRY_LOCAL_v001: