From 6cdebc31548910ae7781bb5a6746c3549606de66 Mon Sep 17 00:00:00 2001 From: Alberto Sala Date: Wed, 15 Apr 2026 15:46:20 +0200 Subject: [PATCH 01/25] Added system test for happy path of erc20 flow --- .../system_tests/payment_app_system_test.go | 257 ++++++++++++++++-- 1 file changed, 239 insertions(+), 18 deletions(-) diff --git a/runtime/wasm-go/system_tests/payment_app_system_test.go b/runtime/wasm-go/system_tests/payment_app_system_test.go index c0543ca..b90476d 100644 --- a/runtime/wasm-go/system_tests/payment_app_system_test.go +++ b/runtime/wasm-go/system_tests/payment_app_system_test.go @@ -14,6 +14,7 @@ import ( "os/exec" "path/filepath" "runtime" + "strings" "testing" "time" @@ -27,33 +28,39 @@ import ( "github.com/stretchr/testify/require" ) -// host-side event types for test validation (app-specific, not framework types) +// host-side event types for test validation (app-specific, not framework types). +// The guest always emits TokenAddress on deposit/withdrawal events — 0x0 for ETH, +// non-zero for ERC-20. Host types include the field so ERC-20 tests can assert it; +// ETH tests see a zero-value Address, which is the correct semantic. type hostDepositEvent struct { - Type string `json:"type"` - Amount *common.Big `json:"amount"` - Balance *common.Big `json:"balance"` - Nonce uint64 `json:"nonce"` + Type string `json:"type"` + TokenAddress ethCommon.Address `json:"tokenAddress"` + Amount *common.Big `json:"amount"` + Balance *common.Big `json:"balance"` + Nonce uint64 `json:"nonce"` } type hostWithdrawalEvent struct { - Type string `json:"type"` - To ethCommon.Address `json:"to"` - Amount *common.Big `json:"amount"` - Balance *common.Big `json:"balance"` - Nonce uint64 `json:"nonce"` + Type string `json:"type"` + To ethCommon.Address `json:"to"` + TokenAddress ethCommon.Address `json:"tokenAddress"` + Amount *common.Big `json:"amount"` + Balance *common.Big `json:"balance"` + Nonce uint64 `json:"nonce"` } // depositToPaymentApp is a helper function to deposit funds and validate the deposit event. +// tokenAddress = 0x0 for ETH, non-zero for ERC-20. // When useSeed is true the event is matched via the hashed subtype set (seed-registered user). // When useSeed is false the event is matched via the plaintext "deposit" subtype (no-seed user). -func depositToPaymentApp(t *testing.T, suite *systemTests.SystemTestSuite, cryptoHelper *systemTests.CryptoHelper, appID common.ApplicationIdType, reqID common.RequestIdType, user ethCommon.Address, amount *big.Int, useSeed bool) { +func depositToPaymentApp(t *testing.T, suite *systemTests.SystemTestSuite, cryptoHelper *systemTests.CryptoHelper, appID common.ApplicationIdType, reqID common.RequestIdType, user ethCommon.Address, tokenAddress ethCommon.Address, amount *big.Int, useSeed bool) { t.Helper() timeout := 100 * time.Second executorPubKey, err := suite.GetExecutorCommunicationKey() require.NoError(t, err) - depositReq, err := cryptoHelper.CreateDepositRequest(appID, reqID, user, amount, executorPubKey) + depositReq, err := cryptoHelper.CreateTokenDepositRequest(appID, reqID, user, tokenAddress, amount, executorPubKey) require.NoError(t, err) require.NoError(t, suite.SubmitRequest(depositReq)) require.NoError(t, suite.AssertRequestCompleted(reqID, timeout)) @@ -76,6 +83,8 @@ func depositToPaymentApp(t *testing.T, suite *systemTests.SystemTestSuite, crypt err = json.Unmarshal(decryptedData, &eventData) require.NoError(t, err) require.Equal(t, "deposit", eventData.Type) + require.Equal(t, tokenAddress, eventData.TokenAddress, + "deposit event must carry the correct tokenAddress (0x0 for ETH)") require.Equal(t, 0, amount.Cmp(eventData.Amount.ToInt())) executorSigningKey, err := suite.GetExecutorSigningKey() @@ -87,14 +96,15 @@ func depositToPaymentApp(t *testing.T, suite *systemTests.SystemTestSuite, crypt } // withdrawFromPaymentApp is a helper function to withdraw funds and validate the withdrawal event. -func withdrawFromPaymentApp(t *testing.T, suite *systemTests.SystemTestSuite, cryptoHelper *systemTests.CryptoHelper, appID common.ApplicationIdType, reqID common.RequestIdType, user, recipient ethCommon.Address, amount *big.Int) { +// tokenAddress = 0x0 for ETH, non-zero for ERC-20. +func withdrawFromPaymentApp(t *testing.T, suite *systemTests.SystemTestSuite, cryptoHelper *systemTests.CryptoHelper, appID common.ApplicationIdType, reqID common.RequestIdType, user, recipient ethCommon.Address, tokenAddress ethCommon.Address, amount *big.Int) { t.Helper() timeout := 100 * time.Second executorPubKey, err := suite.GetExecutorCommunicationKey() require.NoError(t, err) - withdrawalReq, err := cryptoHelper.CreateWithdrawalRequest(appID, reqID, user, recipient, common.ToBig(amount), executorPubKey) + withdrawalReq, err := cryptoHelper.CreateTokenWithdrawalRequest(appID, reqID, user, recipient, tokenAddress, common.ToBig(amount), executorPubKey) require.NoError(t, err) require.NoError(t, suite.SubmitRequest(withdrawalReq)) require.NoError(t, suite.AssertRequestCompleted(reqID, timeout)) @@ -111,13 +121,17 @@ func withdrawFromPaymentApp(t *testing.T, suite *systemTests.SystemTestSuite, cr require.NoError(t, err) require.Equal(t, "withdrawal", eventData.Type) require.Equal(t, recipient, eventData.To) + require.Equal(t, tokenAddress, eventData.TokenAddress, + "withdrawal event must carry the correct tokenAddress (0x0 for ETH)") require.Equal(t, 0, amount.Cmp(eventData.Amount.ToInt())) - // Verify on-chain withdrawal was recorded + // Verify mock-chain Withdrawal record withdrawal, err := suite.WaitForWithdrawal(appID, timeout) require.NoError(t, err) require.NotNil(t, withdrawal) require.Equal(t, recipient, withdrawal.DestinationAddress) + require.Equal(t, tokenAddress, withdrawal.TokenAddress, + "mock-chain Withdrawal record must carry the correct tokenAddress (0x0 for ETH)") require.Equal(t, 0, amount.Cmp(withdrawal.Amount.ToInt())) executorSigningKey, err := suite.GetExecutorSigningKey() @@ -216,6 +230,41 @@ func uploadArtifactAndBuildDescriptorPayload(t *testing.T, suite *systemTests.Sy return payload } +// buildDeployDescriptorWithTokens uploads a WASM artifact and returns a deploy +// descriptor payload (JSON) whose ConstructorParams.allowedTokens lists the +// given token addresses (lowercase hex). Matches the shape the wallet's +// `deployapp --allowed-tokens` CLI produces. +// +// Passing an empty slice is equivalent to uploadArtifactAndBuildDescriptorPayload +// (no ConstructorParams field in the output). +func buildDeployDescriptorWithTokens(t *testing.T, suite *systemTests.SystemTestSuite, wasmBytecode []byte, allowedTokens []ethCommon.Address) []byte { + t.Helper() + + // Reuse the existing uploader, then layer ConstructorParams on top. + basePayload := uploadArtifactAndBuildDescriptorPayload(t, suite, wasmBytecode) + if len(allowedTokens) == 0 { + return basePayload + } + + var descriptor common.DeployDescriptor + require.NoError(t, json.Unmarshal(basePayload, &descriptor)) + + resolved := make([]string, 0, len(allowedTokens)) + for _, addr := range allowedTokens { + resolved = append(resolved, strings.ToLower(addr.Hex())) + } + params := struct { + AllowedTokens []string `json:"allowedTokens"` + }{AllowedTokens: resolved} + ctorBytes, err := json.Marshal(params) + require.NoError(t, err) + descriptor.ConstructorParams = ctorBytes + + payload, err := json.Marshal(descriptor) + require.NoError(t, err) + return payload +} + func TestPaymentAppFullFlow(t *testing.T) { if os.Getenv("CI_FLAG") != "" { t.Skip("Skipping long running test in CI environment") @@ -292,11 +341,11 @@ func TestPaymentAppFullFlow(t *testing.T) { // Deposit 2 ETH and validate event fields (seed-registered user -> hashed subtypes) depositAmount := big.NewInt(2000000000000000000) - depositToPaymentApp(t, suite, cryptoHelper, appID, commontestutil.GenerateRandomRequestID(), userAddress, depositAmount, true) + depositToPaymentApp(t, suite, cryptoHelper, appID, commontestutil.GenerateRandomRequestID(), userAddress, ethCommon.Address{}, depositAmount, true) // Withdraw 0.5 ETH and validate event fields withdrawAmount := big.NewInt(500000000000000000) - withdrawFromPaymentApp(t, suite, cryptoHelper, appID, commontestutil.GenerateRandomRequestID(), userAddress, recipientAddress, withdrawAmount) + withdrawFromPaymentApp(t, suite, cryptoHelper, appID, commontestutil.GenerateRandomRequestID(), userAddress, recipientAddress, ethCommon.Address{}, withdrawAmount) // --- No-seed user: register without a seed, deposit, and verify that // WaitForEvent with the plaintext "deposit" subtype works. When no seed @@ -324,7 +373,7 @@ func TestPaymentAppFullFlow(t *testing.T) { // Deposit 1 ETH for the no-seed user; plaintext "deposit" subtype should match noSeedDepositAmount := big.NewInt(1000000000000000000) - depositToPaymentApp(t, suite, cryptoHelper, appID, commontestutil.GenerateRandomRequestID(), noSeedUser, noSeedDepositAmount, false) + depositToPaymentApp(t, suite, cryptoHelper, appID, commontestutil.GenerateRandomRequestID(), noSeedUser, ethCommon.Address{}, noSeedDepositAmount, false) // Deanonymization report as auditor — verifies final state after deposit and withdrawal reqID = commontestutil.GenerateRandomRequestID() @@ -390,6 +439,178 @@ func TestPaymentAppFullFlow(t *testing.T) { } } +// ---------------------------------------------------------------------------- +// ERC-20 tests +// ---------------------------------------------------------------------------- +// +// Scope: these tests exercise the ERC-20 flow through the manager + executor + +// TinyGo WASM stack. SystemTestSuite uses an in-memory MockClient (not a +// simulated blockchain), so they cover: +// +// - Deploy with ConstructorParams.AllowedTokens populated +// - Requests carrying TokenAddress (deposit / withdrawal / transfer) +// - Guest-side AllowedTokens validation +// - Decrypted event fields (tokenAddress, amount, balance) +// - Mock-chain Withdrawal records (tokenAddress field) +// +// They do NOT exercise on-chain mechanics: ProcessorEndpoint.submitRequest(), +// EIP-2612 permit verification, ERC-20 permit + transferFrom, appCustody / +// pendingClaims accounting, or claim() execution. Those belong to tests that +// run against a simulated Ethereum backend (SimTestHelper in the blockchain +// package). +// +// Synthetic ERC-20 addresses are used throughout because MockClient doesn't +// validate them against a deployed contract. This is intentional — we're +// testing token-identity plumbing through the WASM/executor/manager stack, +// not a real deployed token. + +// TestPaymentAppERC20FullFlow is the ERC-20 counterpart to +// TestPaymentAppFullFlow: deploy with AllowedTokens → register user → +// ERC-20 deposit → ERC-20 withdrawal → deanonymization report asserts the +// remaining balance is keyed under the token address. +func TestPaymentAppERC20FullFlow(t *testing.T) { + if os.Getenv("CI_FLAG") != "" { + t.Skip("Skipping long running test in CI environment") + } + + t.Setenv("MANAGER_ARTIFACTS_PATH", t.TempDir()) + + suite := systemTests.NewSystemTestSuite(t, "wasmtime-payment-erc20", newNetworkLogConfig(), newNetworkLogConfig()) + defer suite.Cleanup() + + wasmBytecode := buildAndLoadWasmModule(t) + + require.NoError(t, suite.StartExecutor()) + require.NoError(t, suite.StartManager()) + + appID := common.NewApplicationId(1) + timeout := 100 * time.Second + + // Synthetic ERC-20 token address. Not a deployed contract — see scope + // comment at top of this section. + tokenAddress := ethCommon.HexToAddress("0x000000000000000000000000000000000000C0DE") + recipientAddress := ethCommon.HexToAddress("0x1234567890123456789012345678901234567890") + + cryptoHelper := systemTests.NewCryptoHelper() + userAddress, err := cryptoHelper.GenerateUserIdentity() + require.NoError(t, err) + auditorAddress, err := cryptoHelper.GenerateUserIdentity() + require.NoError(t, err) + + // --- Deploy with AllowedTokens = [tokenAddress] --- + deployPayload := buildDeployDescriptorWithTokens(t, suite, wasmBytecode, []ethCommon.Address{tokenAddress}) + deployReq := &common.Request{ + RequestType: common.Deploy, + ApplicationID: appID, + RequestID: commontestutil.GenerateRandomRequestID(), + Payload: deployPayload, + Sender: userAddress, + Timestamp: common.ToBig(new(big.Int).SetInt64(time.Now().Unix())), + AssetAmount: common.NewBig(0), + MaxFeeValue: common.NewBig(100), + } + require.NoError(t, suite.SubmitRequest(deployReq)) + _, err = suite.WaitForAppStateInDB(appID, timeout) + require.NoError(t, err) + _, err = suite.WaitForAppStateInBlockchain(appID, timeout) + require.NoError(t, err) + + // The stored ApplicationState.EncryptedState is encrypted by the executor; + // we can't read AllowedTokens directly. Token allowlisting is validated + // implicitly: if the guest didn't receive ConstructorParams.AllowedTokens + // containing tokenAddress, the deposit below would be rejected by the + // guest and AssertRequestCompleted would fail. The decrypted deanonymization + // report at the end also shows the balance keyed under tokenAddress. + ethHex := strings.ToLower(ethCommon.Address{}.Hex()) + tokenHex := strings.ToLower(tokenAddress.Hex()) + + // --- Register user and auditor keys --- + executorPubKey, err := suite.GetExecutorCommunicationKey() + require.NoError(t, err) + + userKey, err := cryptoHelper.GenerateUserKey(userAddress) + require.NoError(t, err) + reqID := commontestutil.GenerateRandomRequestID() + associateKeyReq, err := cryptoHelper.CreateAssociateKeyRequest(appID, reqID, userAddress, userKey.PublicKey(), executorPubKey) + require.NoError(t, err) + require.NoError(t, suite.SubmitRequest(associateKeyReq)) + require.NoError(t, suite.AssertRequestCompleted(reqID, timeout)) + + auditorKey, err := cryptoHelper.GenerateUserKey(auditorAddress) + require.NoError(t, err) + reqID = commontestutil.GenerateRandomRequestID() + associateAuditorReq, err := cryptoHelper.CreateAssociateKeyRequest(appID, reqID, auditorAddress, auditorKey.PublicKey(), executorPubKey) + require.NoError(t, err) + require.NoError(t, suite.SubmitRequest(associateAuditorReq)) + require.NoError(t, suite.AssertRequestCompleted(reqID, timeout)) + + // --- Deposit the ERC-20 token --- + depositAmount := big.NewInt(1_000_000) // arbitrary; token decimals are irrelevant here + depositToPaymentApp(t, suite, cryptoHelper, appID, commontestutil.GenerateRandomRequestID(), userAddress, tokenAddress, depositAmount, true) + + // --- Withdraw a portion --- + withdrawAmount := big.NewInt(400_000) + withdrawFromPaymentApp(t, suite, cryptoHelper, appID, commontestutil.GenerateRandomRequestID(), userAddress, recipientAddress, tokenAddress, withdrawAmount) + + // --- Deanonymization report: remaining balance keyed under the token --- + reqID = commontestutil.GenerateRandomRequestID() + deanonReq, err := cryptoHelper.CreateDeanonymizationRequest(appID, reqID, auditorAddress, []byte("{}"), executorPubKey) + require.NoError(t, err) + require.NoError(t, suite.SubmitRequest(deanonReq)) + require.NoError(t, suite.AssertRequestCompleted(reqID, timeout)) + + deanonReport, err := suite.WaitForDeanonymizationReport(reqID, timeout) + require.NoError(t, err) + require.NotNil(t, deanonReport) + + decryptedReport, err := cryptoHelper.DecryptDeanonymizationReport(auditorAddress, deanonReport, executorPubKey) + require.NoError(t, err) + + var report struct { + ApplicationId common.ApplicationIdType `json:"applicationId"` + RequestId common.RequestIdType `json:"requestId"` + ReportDataBytes interface{} `json:"reportDataBytes"` + } + require.NoError(t, json.Unmarshal(decryptedReport, &report)) + require.Equal(t, appID, report.ApplicationId) + require.Equal(t, reqID, report.RequestId) + + jsonStr, ok := report.ReportDataBytes.(string) + require.True(t, ok, "reportDataBytes is not a string") + reportBytes, err := base64.StdEncoding.DecodeString(jsonStr) + require.NoError(t, err, "reportDataBytes is not base64 encoded") + + var reportData map[string]interface{} + require.NoError(t, json.Unmarshal(reportBytes, &reportData)) + accounts, ok := reportData["accounts"].(map[string]interface{}) + require.True(t, ok, "accounts is not a map") + require.Len(t, accounts, 1, "expected exactly one account in report") + + expectedRemaining := new(big.Int).Sub(depositAmount, withdrawAmount) + for addrHex, acct := range accounts { + acctMap, ok := acct.(map[string]interface{}) + require.True(t, ok, "account entry is not a map") + balances, ok := acctMap["balances"].(map[string]interface{}) + require.True(t, ok, "balances is not a map") + + // Remaining balance must be keyed under the ERC-20 token, not ETH. + balanceStr, ok := balances[tokenHex].(string) + require.True(t, ok, "balance for token %s missing on account %s", tokenHex, addrHex) + require.True(t, len(balanceStr) > 2 && balanceStr[:2] == "0x", "balance is not hex-prefixed") + parsed, ok := new(big.Int).SetString(balanceStr[2:], 16) + require.True(t, ok, "failed to parse balance hex") + require.Equal(t, 0, expectedRemaining.Cmp(parsed), + "account %s: expected token balance %s, got %s", addrHex, expectedRemaining, parsed) + + // No ETH deposit was made; any ETH entry must be zero. + if ethBalanceStr, hasETH := balances[ethHex].(string); hasETH { + ethParsed, _ := new(big.Int).SetString(ethBalanceStr[2:], 16) + require.Equal(t, 0, ethParsed.Sign(), + "account %s should have no ETH balance, got %s", addrHex, ethBalanceStr) + } + } +} + // newNetworkLogConfig returns a zeronetwork logger config. // The suite injects the correct log-server port into RemoteLogParams before // creating the logger, so no hardcoded port is needed here. From e15d6b2b517cd39fac084371290a7cf035100502 Mon Sep 17 00:00:00 2001 From: Alberto Sala Date: Wed, 15 Apr 2026 17:59:20 +0200 Subject: [PATCH 02/25] Added more unit tests and applied some code refactoring to reduce duplications --- .../system_tests/payment_app_system_test.go | 727 ++++++++++++++---- 1 file changed, 567 insertions(+), 160 deletions(-) diff --git a/runtime/wasm-go/system_tests/payment_app_system_test.go b/runtime/wasm-go/system_tests/payment_app_system_test.go index b90476d..071f73b 100644 --- a/runtime/wasm-go/system_tests/payment_app_system_test.go +++ b/runtime/wasm-go/system_tests/payment_app_system_test.go @@ -18,8 +18,10 @@ import ( "testing" "time" + "github.com/HorizenOfficial/vela-nova/payment-app/app" "github.com/HorizenOfficial/vela/pkg/authorityservice/deployartifact" "github.com/HorizenOfficial/vela/pkg/common" + cryptotypes "github.com/HorizenOfficial/vela/pkg/common/crypto" commontestutil "github.com/HorizenOfficial/vela/pkg/common/testutil" "github.com/HorizenOfficial/vela/pkg/executor" "github.com/HorizenOfficial/vela/pkg/logger" @@ -49,6 +51,30 @@ type hostWithdrawalEvent struct { Nonce uint64 `json:"nonce"` } +// hostSenderEvent mirrors the guest SenderEvent emitted to the sender of a +// private transfer. Balance is the sender's balance AFTER the transfer. +type hostSenderEvent struct { + Type string `json:"type"` + To ethCommon.Address `json:"to"` + TokenAddress ethCommon.Address `json:"tokenAddress"` + Amount *common.Big `json:"amount"` + Balance *common.Big `json:"balance"` + Nonce uint64 `json:"nonce"` + InvoiceID string `json:"invoice_id,omitempty"` +} + +// hostRecipientEvent mirrors the guest RecipientEvent emitted to the recipient +// of a private transfer. Balance is the recipient's balance AFTER the transfer. +type hostRecipientEvent struct { + Type string `json:"type"` + From ethCommon.Address `json:"from"` + TokenAddress ethCommon.Address `json:"tokenAddress"` + Amount *common.Big `json:"amount"` + Balance *common.Big `json:"balance"` + Nonce uint64 `json:"nonce"` + InvoiceID string `json:"invoice_id,omitempty"` +} + // depositToPaymentApp is a helper function to deposit funds and validate the deposit event. // tokenAddress = 0x0 for ETH, non-zero for ERC-20. // When useSeed is true the event is matched via the hashed subtype set (seed-registered user). @@ -253,10 +279,7 @@ func buildDeployDescriptorWithTokens(t *testing.T, suite *systemTests.SystemTest for _, addr := range allowedTokens { resolved = append(resolved, strings.ToLower(addr.Hex())) } - params := struct { - AllowedTokens []string `json:"allowedTokens"` - }{AllowedTokens: resolved} - ctorBytes, err := json.Marshal(params) + ctorBytes, err := json.Marshal(app.DeployParams{AllowedTokens: resolved}) require.NoError(t, err) descriptor.ConstructorParams = ctorBytes @@ -265,6 +288,104 @@ func buildDeployDescriptorWithTokens(t *testing.T, suite *systemTests.SystemTest return payload } +// deployAppWithTokens submits a Deploy request for appID with the given allowed-tokens +// list (empty = ETH only, no ConstructorParams) and waits for the app to appear in +// both the manager DB and the mock blockchain. +func deployAppWithTokens(t *testing.T, suite *systemTests.SystemTestSuite, appID common.ApplicationIdType, sender ethCommon.Address, wasmBytecode []byte, allowedTokens []ethCommon.Address, timeout time.Duration) { + t.Helper() + deployReq := &common.Request{ + RequestType: common.Deploy, + ApplicationID: appID, + RequestID: commontestutil.GenerateRandomRequestID(), + Payload: buildDeployDescriptorWithTokens(t, suite, wasmBytecode, allowedTokens), + Sender: sender, + Timestamp: common.ToBig(new(big.Int).SetInt64(time.Now().Unix())), + AssetAmount: common.NewBig(0), + MaxFeeValue: common.NewBig(100), + } + require.NoError(t, suite.SubmitRequest(deployReq)) + _, err := suite.WaitForAppStateInDB(appID, timeout) + require.NoError(t, err) + _, err = suite.WaitForAppStateInBlockchain(appID, timeout) + require.NoError(t, err) +} + +// registerSeedUser generates a user key and submits an AssociateKey request that +// embeds the encrypted secp256k1 seed — events to this user are routed via the +// hashed-subtype scheme (WaitForEventBySubtypes with AllSubtypes(seed, N)). +func registerSeedUser(t *testing.T, suite *systemTests.SystemTestSuite, cryptoHelper *systemTests.CryptoHelper, executorPubKey *cryptotypes.PublicKeyP521, appID common.ApplicationIdType, user ethCommon.Address, timeout time.Duration) { + t.Helper() + userKey, err := cryptoHelper.GenerateUserKey(user) + require.NoError(t, err) + reqID := commontestutil.GenerateRandomRequestID() + req, err := cryptoHelper.CreateAssociateKeyRequest(appID, reqID, user, userKey.PublicKey(), executorPubKey) + require.NoError(t, err) + require.NoError(t, suite.SubmitRequest(req)) + require.NoError(t, suite.AssertRequestCompleted(reqID, timeout)) +} + +// registerNoSeedUser submits an AssociateKey request carrying only the 133-byte +// P521 public key (no encrypted seed). The framework preserves the WASM-provided +// plaintext subtype when routing events to this user. +func registerNoSeedUser(t *testing.T, suite *systemTests.SystemTestSuite, cryptoHelper *systemTests.CryptoHelper, appID common.ApplicationIdType, user ethCommon.Address, timeout time.Duration) { + t.Helper() + userKey, err := cryptoHelper.GenerateUserKey(user) + require.NoError(t, err) + reqID := commontestutil.GenerateRandomRequestID() + req := &common.Request{ + ApplicationID: appID, + RequestID: reqID, + RequestType: common.AssociateKey, + Payload: userKey.PublicKey().Bytes(), // 133 bytes, no encrypted seed + Sender: user, + Timestamp: common.ToBig(new(big.Int).SetInt64(time.Now().Unix())), + AssetAmount: common.NewBig(0), + TokenAddress: ethCommon.Address{}, + MaxFeeValue: common.NewBig(100), + } + require.NoError(t, suite.SubmitRequest(req)) + require.NoError(t, suite.AssertRequestCompleted(reqID, timeout)) +} + +// fetchDeanonAccounts submits a deanonymization request from `auditor`, waits +// for the report, decrypts it, asserts the envelope's appID and requestID +// round-trip correctly, and returns the parsed accounts map. +func fetchDeanonAccounts(t *testing.T, suite *systemTests.SystemTestSuite, cryptoHelper *systemTests.CryptoHelper, executorPubKey *cryptotypes.PublicKeyP521, appID common.ApplicationIdType, auditor ethCommon.Address, timeout time.Duration) map[string]interface{} { + t.Helper() + reqID := commontestutil.GenerateRandomRequestID() + req, err := cryptoHelper.CreateDeanonymizationRequest(appID, reqID, auditor, []byte("{}"), executorPubKey) + require.NoError(t, err) + require.NoError(t, suite.SubmitRequest(req)) + require.NoError(t, suite.AssertRequestCompleted(reqID, timeout)) + + report, err := suite.WaitForDeanonymizationReport(reqID, timeout) + require.NoError(t, err) + require.NotNil(t, report) + + decrypted, err := cryptoHelper.DecryptDeanonymizationReport(auditor, report, executorPubKey) + require.NoError(t, err) + + var envelope struct { + ApplicationId common.ApplicationIdType `json:"applicationId"` + RequestId common.RequestIdType `json:"requestId"` + ReportDataBytes interface{} `json:"reportDataBytes"` + } + require.NoError(t, json.Unmarshal(decrypted, &envelope)) + require.Equal(t, appID, envelope.ApplicationId, "deanon report envelope appID must round-trip") + require.Equal(t, reqID, envelope.RequestId, "deanon report envelope requestID must round-trip") + + jsonStr, ok := envelope.ReportDataBytes.(string) + require.True(t, ok, "reportDataBytes is not a string") + reportBytes, err := base64.StdEncoding.DecodeString(jsonStr) + require.NoError(t, err, "reportDataBytes is not base64 encoded") + + var data map[string]interface{} + require.NoError(t, json.Unmarshal(reportBytes, &data)) + accounts, ok := data["accounts"].(map[string]interface{}) + require.True(t, ok, "accounts is not a map") + return accounts +} + func TestPaymentAppFullFlow(t *testing.T) { if os.Getenv("CI_FLAG") != "" { t.Skip("Skipping long running test in CI environment") @@ -299,45 +420,15 @@ func TestPaymentAppFullFlow(t *testing.T) { require.NoError(t, err) recipientAddress := ethCommon.HexToAddress("0x1234567890123456789012345678901234567890") - // Deploy the application using deploy descriptor (upload artifact first) - deployPayload := uploadArtifactAndBuildDescriptorPayload(t, suite, wasmBytecode) - deployReq := &common.Request{ - RequestType: common.Deploy, - ApplicationID: appID, - RequestID: commontestutil.GenerateRandomRequestID(), - Payload: deployPayload, - Sender: userAddress, - Timestamp: common.ToBig(new(big.Int).SetInt64(time.Now().Unix())), - AssetAmount: common.NewBig(0), - MaxFeeValue: common.NewBig(100), - } - require.NoError(t, suite.SubmitRequest(deployReq)) - _, err = suite.WaitForAppStateInDB(appID, timeout) - require.NoError(t, err) - _, err = suite.WaitForAppStateInBlockchain(appID, timeout) - require.NoError(t, err) + // Deploy the application (ETH-only: no ConstructorParams) + deployAppWithTokens(t, suite, appID, userAddress, wasmBytecode, nil, timeout) - // Get executor communication key for associate key requests executorPubKey, err := suite.GetExecutorCommunicationKey() require.NoError(t, err) - // Register user key - userKey, err := cryptoHelper.GenerateUserKey(userAddress) - require.NoError(t, err) - reqID := commontestutil.GenerateRandomRequestID() - associateKeyReq, err := cryptoHelper.CreateAssociateKeyRequest(appID, reqID, userAddress, userKey.PublicKey(), executorPubKey) - require.NoError(t, err) - require.NoError(t, suite.SubmitRequest(associateKeyReq)) - require.NoError(t, suite.AssertRequestCompleted(reqID, timeout)) - - // Register auditor key - auditorKey, err := cryptoHelper.GenerateUserKey(auditorAddress) - require.NoError(t, err) - reqID = commontestutil.GenerateRandomRequestID() - associateAuditorReq, err := cryptoHelper.CreateAssociateKeyRequest(appID, reqID, auditorAddress, auditorKey.PublicKey(), executorPubKey) - require.NoError(t, err) - require.NoError(t, suite.SubmitRequest(associateAuditorReq)) - require.NoError(t, suite.AssertRequestCompleted(reqID, timeout)) + // Register user and auditor keys (both seed-registered) + registerSeedUser(t, suite, cryptoHelper, executorPubKey, appID, userAddress, timeout) + registerSeedUser(t, suite, cryptoHelper, executorPubKey, appID, auditorAddress, timeout) // Deposit 2 ETH and validate event fields (seed-registered user -> hashed subtypes) depositAmount := big.NewInt(2000000000000000000) @@ -352,67 +443,15 @@ func TestPaymentAppFullFlow(t *testing.T) { // is registered the framework preserves the WASM-provided subtype as-is. noSeedUser, err := cryptoHelper.GenerateUserIdentity() require.NoError(t, err) - noSeedKey, err := cryptoHelper.GenerateUserKey(noSeedUser) - require.NoError(t, err) - - // Build an AssociateKey request with only the P521 public key (133 bytes, no seed) - reqID = commontestutil.GenerateRandomRequestID() - noSeedAssocReq := &common.Request{ - ApplicationID: appID, - RequestID: reqID, - RequestType: common.AssociateKey, - Payload: noSeedKey.PublicKey().Bytes(), // 133 bytes, no encrypted seed - Sender: noSeedUser, - Timestamp: common.ToBig(new(big.Int).SetInt64(time.Now().Unix())), - AssetAmount: common.NewBig(0), - TokenAddress: ethCommon.Address{}, - MaxFeeValue: common.NewBig(100), - } - require.NoError(t, suite.SubmitRequest(noSeedAssocReq)) - require.NoError(t, suite.AssertRequestCompleted(reqID, timeout)) + registerNoSeedUser(t, suite, cryptoHelper, appID, noSeedUser, timeout) // Deposit 1 ETH for the no-seed user; plaintext "deposit" subtype should match noSeedDepositAmount := big.NewInt(1000000000000000000) depositToPaymentApp(t, suite, cryptoHelper, appID, commontestutil.GenerateRandomRequestID(), noSeedUser, ethCommon.Address{}, noSeedDepositAmount, false) - // Deanonymization report as auditor — verifies final state after deposit and withdrawal - reqID = commontestutil.GenerateRandomRequestID() - deanonReq, err := cryptoHelper.CreateDeanonymizationRequest(appID, reqID, auditorAddress, []byte("{}"), executorPubKey) - require.NoError(t, err) - require.NoError(t, suite.SubmitRequest(deanonReq)) - require.NoError(t, suite.AssertRequestCompleted(reqID, timeout)) - - deanonReport, err := suite.WaitForDeanonymizationReport(reqID, timeout) - require.NoError(t, err) - require.NotNil(t, deanonReport) - - decryptedReport, err := cryptoHelper.DecryptDeanonymizationReport(auditorAddress, deanonReport, executorPubKey) - require.NoError(t, err) - - var report struct { - ApplicationId common.ApplicationIdType `json:"applicationId"` - RequestId common.RequestIdType `json:"requestId"` - ReportDataBytes interface{} `json:"reportDataBytes"` - } - err = json.Unmarshal(decryptedReport, &report) - require.NoError(t, err) - require.Equal(t, appID, report.ApplicationId) - require.Equal(t, reqID, report.RequestId) - - jsonStr, ok := report.ReportDataBytes.(string) - require.True(t, ok, "reportDataBytes is not a string") - reportBytes, err := base64.StdEncoding.DecodeString(jsonStr) - require.NoError(t, err, "reportDataBytes is not base64 encoded") - - var reportData map[string]interface{} - err = json.Unmarshal(reportBytes, &reportData) - require.NoError(t, err) - require.Contains(t, reportData, "accounts") - require.Contains(t, reportData, "nonce") - + // Deanonymization report as auditor — verifies final state after deposit and withdrawal. // Verify balances: seed user (2 ETH - 0.5 ETH = 1.5 ETH) and no-seed user (1 ETH) - accounts, ok := reportData["accounts"].(map[string]interface{}) - require.True(t, ok, "accounts is not a map") + accounts := fetchDeanonAccounts(t, suite, cryptoHelper, executorPubKey, appID, auditorAddress, timeout) require.Len(t, accounts, 2, "expected two accounts in report (seed user + no-seed user)") ethTokenHex := ethCommon.Address{}.Hex() @@ -427,7 +466,7 @@ func TestPaymentAppFullFlow(t *testing.T) { require.True(t, ok, "balances is not a map") balanceStr, ok := balances[ethTokenHex].(string) require.True(t, ok, "ETH balance is not a string for account %s", addrHex) - require.True(t, len(balanceStr) > 2 && balanceStr[:2] == "0x", "balance is not hex") + require.True(t, strings.HasPrefix(balanceStr, "0x"), "balance is not hex") balance, ok := new(big.Int).SetString(balanceStr[2:], 16) require.True(t, ok, "failed to parse balance hex") @@ -498,22 +537,7 @@ func TestPaymentAppERC20FullFlow(t *testing.T) { require.NoError(t, err) // --- Deploy with AllowedTokens = [tokenAddress] --- - deployPayload := buildDeployDescriptorWithTokens(t, suite, wasmBytecode, []ethCommon.Address{tokenAddress}) - deployReq := &common.Request{ - RequestType: common.Deploy, - ApplicationID: appID, - RequestID: commontestutil.GenerateRandomRequestID(), - Payload: deployPayload, - Sender: userAddress, - Timestamp: common.ToBig(new(big.Int).SetInt64(time.Now().Unix())), - AssetAmount: common.NewBig(0), - MaxFeeValue: common.NewBig(100), - } - require.NoError(t, suite.SubmitRequest(deployReq)) - _, err = suite.WaitForAppStateInDB(appID, timeout) - require.NoError(t, err) - _, err = suite.WaitForAppStateInBlockchain(appID, timeout) - require.NoError(t, err) + deployAppWithTokens(t, suite, appID, userAddress, wasmBytecode, []ethCommon.Address{tokenAddress}, timeout) // The stored ApplicationState.EncryptedState is encrypted by the executor; // we can't read AllowedTokens directly. Token allowlisting is validated @@ -527,22 +551,8 @@ func TestPaymentAppERC20FullFlow(t *testing.T) { // --- Register user and auditor keys --- executorPubKey, err := suite.GetExecutorCommunicationKey() require.NoError(t, err) - - userKey, err := cryptoHelper.GenerateUserKey(userAddress) - require.NoError(t, err) - reqID := commontestutil.GenerateRandomRequestID() - associateKeyReq, err := cryptoHelper.CreateAssociateKeyRequest(appID, reqID, userAddress, userKey.PublicKey(), executorPubKey) - require.NoError(t, err) - require.NoError(t, suite.SubmitRequest(associateKeyReq)) - require.NoError(t, suite.AssertRequestCompleted(reqID, timeout)) - - auditorKey, err := cryptoHelper.GenerateUserKey(auditorAddress) - require.NoError(t, err) - reqID = commontestutil.GenerateRandomRequestID() - associateAuditorReq, err := cryptoHelper.CreateAssociateKeyRequest(appID, reqID, auditorAddress, auditorKey.PublicKey(), executorPubKey) - require.NoError(t, err) - require.NoError(t, suite.SubmitRequest(associateAuditorReq)) - require.NoError(t, suite.AssertRequestCompleted(reqID, timeout)) + registerSeedUser(t, suite, cryptoHelper, executorPubKey, appID, userAddress, timeout) + registerSeedUser(t, suite, cryptoHelper, executorPubKey, appID, auditorAddress, timeout) // --- Deposit the ERC-20 token --- depositAmount := big.NewInt(1_000_000) // arbitrary; token decimals are irrelevant here @@ -553,37 +563,7 @@ func TestPaymentAppERC20FullFlow(t *testing.T) { withdrawFromPaymentApp(t, suite, cryptoHelper, appID, commontestutil.GenerateRandomRequestID(), userAddress, recipientAddress, tokenAddress, withdrawAmount) // --- Deanonymization report: remaining balance keyed under the token --- - reqID = commontestutil.GenerateRandomRequestID() - deanonReq, err := cryptoHelper.CreateDeanonymizationRequest(appID, reqID, auditorAddress, []byte("{}"), executorPubKey) - require.NoError(t, err) - require.NoError(t, suite.SubmitRequest(deanonReq)) - require.NoError(t, suite.AssertRequestCompleted(reqID, timeout)) - - deanonReport, err := suite.WaitForDeanonymizationReport(reqID, timeout) - require.NoError(t, err) - require.NotNil(t, deanonReport) - - decryptedReport, err := cryptoHelper.DecryptDeanonymizationReport(auditorAddress, deanonReport, executorPubKey) - require.NoError(t, err) - - var report struct { - ApplicationId common.ApplicationIdType `json:"applicationId"` - RequestId common.RequestIdType `json:"requestId"` - ReportDataBytes interface{} `json:"reportDataBytes"` - } - require.NoError(t, json.Unmarshal(decryptedReport, &report)) - require.Equal(t, appID, report.ApplicationId) - require.Equal(t, reqID, report.RequestId) - - jsonStr, ok := report.ReportDataBytes.(string) - require.True(t, ok, "reportDataBytes is not a string") - reportBytes, err := base64.StdEncoding.DecodeString(jsonStr) - require.NoError(t, err, "reportDataBytes is not base64 encoded") - - var reportData map[string]interface{} - require.NoError(t, json.Unmarshal(reportBytes, &reportData)) - accounts, ok := reportData["accounts"].(map[string]interface{}) - require.True(t, ok, "accounts is not a map") + accounts := fetchDeanonAccounts(t, suite, cryptoHelper, executorPubKey, appID, auditorAddress, timeout) require.Len(t, accounts, 1, "expected exactly one account in report") expectedRemaining := new(big.Int).Sub(depositAmount, withdrawAmount) @@ -596,7 +576,7 @@ func TestPaymentAppERC20FullFlow(t *testing.T) { // Remaining balance must be keyed under the ERC-20 token, not ETH. balanceStr, ok := balances[tokenHex].(string) require.True(t, ok, "balance for token %s missing on account %s", tokenHex, addrHex) - require.True(t, len(balanceStr) > 2 && balanceStr[:2] == "0x", "balance is not hex-prefixed") + require.True(t, strings.HasPrefix(balanceStr, "0x"), "balance is not hex-prefixed") parsed, ok := new(big.Int).SetString(balanceStr[2:], 16) require.True(t, ok, "failed to parse balance hex") require.Equal(t, 0, expectedRemaining.Cmp(parsed), @@ -604,13 +584,440 @@ func TestPaymentAppERC20FullFlow(t *testing.T) { // No ETH deposit was made; any ETH entry must be zero. if ethBalanceStr, hasETH := balances[ethHex].(string); hasETH { - ethParsed, _ := new(big.Int).SetString(ethBalanceStr[2:], 16) + ethParsed, ok := new(big.Int).SetString(ethBalanceStr[2:], 16) + require.True(t, ok, "failed to parse ETH balance hex") require.Equal(t, 0, ethParsed.Sign(), "account %s should have no ETH balance, got %s", addrHex, ethBalanceStr) } } } +// TestPaymentAppERC20MultiToken deploys the app with two ERC-20 tokens +// allowlisted, deposits both from the same user, withdraws only one, and +// verifies via the deanonymization report that per-token balances are +// isolated: the withdrawn token shows (deposit - withdrawal), the other +// shows its original deposit untouched. +func TestPaymentAppERC20MultiToken(t *testing.T) { + if os.Getenv("CI_FLAG") != "" { + t.Skip("Skipping long running test in CI environment") + } + + t.Setenv("MANAGER_ARTIFACTS_PATH", t.TempDir()) + + suite := systemTests.NewSystemTestSuite(t, "wasmtime-payment-erc20-multi", newNetworkLogConfig(), newNetworkLogConfig()) + defer suite.Cleanup() + + wasmBytecode := buildAndLoadWasmModule(t) + + require.NoError(t, suite.StartExecutor()) + require.NoError(t, suite.StartManager()) + + appID := common.NewApplicationId(1) + timeout := 100 * time.Second + + // Two synthetic ERC-20 token addresses. Not deployed contracts — see + // scope comment at top of this section. + tokenA := ethCommon.HexToAddress("0x000000000000000000000000000000000000A11A") + tokenB := ethCommon.HexToAddress("0x000000000000000000000000000000000000B22B") + recipientAddress := ethCommon.HexToAddress("0x1234567890123456789012345678901234567890") + + cryptoHelper := systemTests.NewCryptoHelper() + userAddress, err := cryptoHelper.GenerateUserIdentity() + require.NoError(t, err) + auditorAddress, err := cryptoHelper.GenerateUserIdentity() + require.NoError(t, err) + + // --- Deploy with AllowedTokens = [tokenA, tokenB] --- + deployAppWithTokens(t, suite, appID, userAddress, wasmBytecode, []ethCommon.Address{tokenA, tokenB}, timeout) + + tokenAHex := strings.ToLower(tokenA.Hex()) + tokenBHex := strings.ToLower(tokenB.Hex()) + + // --- Register user and auditor keys --- + executorPubKey, err := suite.GetExecutorCommunicationKey() + require.NoError(t, err) + registerSeedUser(t, suite, cryptoHelper, executorPubKey, appID, userAddress, timeout) + registerSeedUser(t, suite, cryptoHelper, executorPubKey, appID, auditorAddress, timeout) + + // --- Deposit both tokens from the same user --- + depositA := big.NewInt(1_000_000) + depositB := big.NewInt(5_500_000) + depositToPaymentApp(t, suite, cryptoHelper, appID, commontestutil.GenerateRandomRequestID(), userAddress, tokenA, depositA, true) + depositToPaymentApp(t, suite, cryptoHelper, appID, commontestutil.GenerateRandomRequestID(), userAddress, tokenB, depositB, true) + + // --- Withdraw tokenA only; the helper asserts the mock-chain Withdrawal + // record carries tokenA as its TokenAddress (not tokenB). + withdrawA := big.NewInt(400_000) + withdrawFromPaymentApp(t, suite, cryptoHelper, appID, commontestutil.GenerateRandomRequestID(), userAddress, recipientAddress, tokenA, withdrawA) + + // --- Deanonymization report: verify per-token isolation --- + accounts := fetchDeanonAccounts(t, suite, cryptoHelper, executorPubKey, appID, auditorAddress, timeout) + require.Len(t, accounts, 1, "expected exactly one account in report") + + expectedA := new(big.Int).Sub(depositA, withdrawA) // tokenA was withdrawn partially + expectedB := new(big.Int).Set(depositB) // tokenB was NOT touched + + for addrHex, acct := range accounts { + acctMap, ok := acct.(map[string]interface{}) + require.True(t, ok, "account entry is not a map") + balances, ok := acctMap["balances"].(map[string]interface{}) + require.True(t, ok, "balances is not a map") + + // tokenA: deposit - withdrawal + balanceAStr, ok := balances[tokenAHex].(string) + require.True(t, ok, "balance for tokenA (%s) missing on account %s", tokenAHex, addrHex) + parsedA, ok := new(big.Int).SetString(balanceAStr[2:], 16) + require.True(t, ok, "failed to parse tokenA balance hex") + require.Equal(t, 0, expectedA.Cmp(parsedA), + "account %s tokenA balance: expected %s, got %s", addrHex, expectedA, parsedA) + + // tokenB: unchanged (the withdrawal of tokenA must not have affected tokenB) + balanceBStr, ok := balances[tokenBHex].(string) + require.True(t, ok, "balance for tokenB (%s) missing on account %s", tokenBHex, addrHex) + parsedB, ok := new(big.Int).SetString(balanceBStr[2:], 16) + require.True(t, ok, "failed to parse tokenB balance hex") + require.Equal(t, 0, expectedB.Cmp(parsedB), + "account %s tokenB balance should be unchanged: expected %s, got %s", addrHex, expectedB, parsedB) + } +} + +// TestPaymentAppERC20MultiUser exercises a private transfer between two users +// in the ERC-20 token. The two users are deliberately in different event- +// routing modes to cover both within a single test: +// +// - userA is SEED-REGISTERED: AssociateKey request carries an encrypted +// seed, and events reach userA via the privacy-preserving hashed-subtype +// set (WaitForEventBySubtypes with AllSubtypes(seed, N)). +// - userB is NO-SEED: AssociateKey request carries only the P521 public +// key (133 bytes, no encrypted seed). Events reach userB via the plain +// subtype the guest emits (WaitForEvent with the literal subtype name). +// +// The transfer goes A -> B, so we verify: +// - userA receives a "transfer_sent" event via the seed path, with correct +// tokenAddress, amount, and post-transfer balance +// - userB receives a "transfer_received" event via the no-seed plain- +// subtype path, with correct tokenAddress, amount, and post-transfer +// balance +// - the deanonymization report shows both accounts with the expected +// balances +// - total conservation: (userA remaining + userB remaining) == sum of the +// two original deposits (no tokens minted or lost by the transfer) +// +// The no-seed user receiving a transfer_received event is new coverage vs +// TestPaymentAppFullFlow, which only exercises the no-seed path for deposit +// events. +func TestPaymentAppERC20MultiUser(t *testing.T) { + if os.Getenv("CI_FLAG") != "" { + t.Skip("Skipping long running test in CI environment") + } + + t.Setenv("MANAGER_ARTIFACTS_PATH", t.TempDir()) + + suite := systemTests.NewSystemTestSuite(t, "wasmtime-payment-erc20-multiuser", newNetworkLogConfig(), newNetworkLogConfig()) + defer suite.Cleanup() + + wasmBytecode := buildAndLoadWasmModule(t) + + require.NoError(t, suite.StartExecutor()) + require.NoError(t, suite.StartManager()) + + appID := common.NewApplicationId(1) + timeout := 100 * time.Second + + tokenAddress := ethCommon.HexToAddress("0x000000000000000000000000000000000000C0DE") + + cryptoHelper := systemTests.NewCryptoHelper() + userA, err := cryptoHelper.GenerateUserIdentity() // seed-registered + require.NoError(t, err) + userB, err := cryptoHelper.GenerateUserIdentity() // no-seed + require.NoError(t, err) + auditorAddress, err := cryptoHelper.GenerateUserIdentity() // seed-registered (needs to decrypt deanon report) + require.NoError(t, err) + + // --- Deploy with AllowedTokens = [tokenAddress] --- + deployAppWithTokens(t, suite, appID, userA, wasmBytecode, []ethCommon.Address{tokenAddress}, timeout) + + tokenHex := strings.ToLower(tokenAddress.Hex()) + + // --- Register keys --- + // userA and auditor: seed-registered (CreateAssociateKeyRequest embeds the + // encrypted seed in the request payload). userB: no-seed (P521 key only). + executorPubKey, err := suite.GetExecutorCommunicationKey() + require.NoError(t, err) + registerSeedUser(t, suite, cryptoHelper, executorPubKey, appID, userA, timeout) + registerSeedUser(t, suite, cryptoHelper, executorPubKey, appID, auditorAddress, timeout) + registerNoSeedUser(t, suite, cryptoHelper, appID, userB, timeout) + + // --- Both users deposit the ERC-20 token (independent amounts) --- + // useSeed=true for userA -> event matched via hashed subtype set + // useSeed=false for userB -> event matched via plain "deposit" subtype + depositA := big.NewInt(3_000_000) + depositB := big.NewInt(2_000_000) + depositToPaymentApp(t, suite, cryptoHelper, appID, commontestutil.GenerateRandomRequestID(), userA, tokenAddress, depositA, true) + depositToPaymentApp(t, suite, cryptoHelper, appID, commontestutil.GenerateRandomRequestID(), userB, tokenAddress, depositB, false) + + // --- Private transfer A -> B --- + transferAmount := big.NewInt(1_200_000) + + // Payload shape matches the guest's PayloadInstructions / TransferInstruction. + transferPayload, err := json.Marshal(map[string]interface{}{ + "type": "transfer", + "transfer": map[string]interface{}{ + "to": userB, + "tokenAddress": tokenAddress, + "amount": common.ToBig(transferAmount), + }, + }) + require.NoError(t, err) + + transferReqID := commontestutil.GenerateRandomRequestID() + transferReq, err := cryptoHelper.CreateProcessRequest(appID, transferReqID, userA, transferPayload, executorPubKey) + require.NoError(t, err) + require.NoError(t, suite.SubmitRequest(transferReq)) + require.NoError(t, suite.AssertRequestCompleted(transferReqID, timeout)) + + // --- Verify sender event (userA, seed path) --- + userASeed, err := cryptoHelper.ComputeSeed(userA) + require.NoError(t, err) + senderEvent, err := suite.WaitForEventBySubtypes(userA, executor.AllSubtypes(userASeed, executor.DefaultSubtypeN), timeout) + require.NoError(t, err) + senderDecrypted, err := cryptoHelper.DecryptEvent(userA, senderEvent, executorPubKey) + require.NoError(t, err) + var senderData hostSenderEvent + require.NoError(t, json.Unmarshal(senderDecrypted, &senderData)) + require.Equal(t, userB, senderData.To, "sender event must record recipient=userB") + require.Equal(t, tokenAddress, senderData.TokenAddress, + "sender event must carry tokenAddress") + require.Equal(t, 0, transferAmount.Cmp(senderData.Amount.ToInt()), + "sender event amount mismatch") + expectedABalance := new(big.Int).Sub(depositA, transferAmount) + require.Equal(t, 0, expectedABalance.Cmp(senderData.Balance.ToInt()), + "userA post-transfer balance: expected %s, got %s", expectedABalance, senderData.Balance.ToInt()) + + // --- Verify recipient event (userB, no-seed plain-subtype path) --- + // The guest emits the recipient event with subtype "transfer_received"; + // with no seed registered the framework preserves that literal subtype. + recipientEvent, err := suite.WaitForEvent(userB, "transfer_received", timeout) + require.NoError(t, err) + recipientDecrypted, err := cryptoHelper.DecryptEvent(userB, recipientEvent, executorPubKey) + require.NoError(t, err) + var recipientData hostRecipientEvent + require.NoError(t, json.Unmarshal(recipientDecrypted, &recipientData)) + require.Equal(t, userA, recipientData.From, "recipient event must record sender=userA") + require.Equal(t, tokenAddress, recipientData.TokenAddress, + "recipient event must carry tokenAddress") + require.Equal(t, 0, transferAmount.Cmp(recipientData.Amount.ToInt()), + "recipient event amount mismatch") + expectedBBalance := new(big.Int).Add(depositB, transferAmount) + require.Equal(t, 0, expectedBBalance.Cmp(recipientData.Balance.ToInt()), + "userB post-transfer balance: expected %s, got %s", expectedBBalance, recipientData.Balance.ToInt()) + + // --- Deanonymization report: both accounts with correct balances --- + accounts := fetchDeanonAccounts(t, suite, cryptoHelper, executorPubKey, appID, auditorAddress, timeout) + require.Len(t, accounts, 2, "expected exactly two accounts (userA + userB)") + + // Walk both accounts and verify per-user balance. Also tally the total + // for conservation check. + total := new(big.Int) + seenA, seenB := false, false + for addrHex, acct := range accounts { + acctMap := acct.(map[string]interface{}) + balances := acctMap["balances"].(map[string]interface{}) + balStr, ok := balances[tokenHex].(string) + require.True(t, ok, "account %s missing balance for token %s", addrHex, tokenHex) + bal, ok := new(big.Int).SetString(balStr[2:], 16) + require.True(t, ok, "failed to parse balance hex for %s", addrHex) + total.Add(total, bal) + + addr := ethCommon.HexToAddress(addrHex) + switch addr { + case userA: + seenA = true + require.Equal(t, 0, expectedABalance.Cmp(bal), + "userA report balance: expected %s, got %s", expectedABalance, bal) + case userB: + seenB = true + require.Equal(t, 0, expectedBBalance.Cmp(bal), + "userB report balance: expected %s, got %s", expectedBBalance, bal) + default: + t.Fatalf("unexpected account %s in report", addrHex) + } + } + require.True(t, seenA, "userA missing from report") + require.True(t, seenB, "userB missing from report") + + // Conservation: total across accounts must equal the sum of the two + // original deposits (no tokens minted or lost by the transfer). + expectedTotal := new(big.Int).Add(depositA, depositB) + require.Equal(t, 0, expectedTotal.Cmp(total), + "total conservation broken: expected %s, got %s", expectedTotal, total) +} + +// TestPaymentAppERC20NegativePath is a table-driven test covering the WASM- +// layer rejections for ERC-20 operations. It runs a single suite with one +// successful baseline deposit, then a series of subtests each submitting a +// request that the guest should reject. +// +// The guest's validation happens BEFORE any state mutation, so all four +// failing subtests must leave state untouched. A final successful deposit +// after the failures proves the app remains functional, and the closing +// deanonymization report asserts the balance equals the baseline plus the +// final deposit (nothing leaked through the failed operations). +// +// Scope note: all subtests exercise guest-side rejections visible to the +// test via AssertRequestCompleted returning "has failed". The mock blockchain +// stores failed requests but not their UpdatePayload, so this test does not +// assert on specific guest error strings — only that the request failed and +// that state is intact after each failure. Contract-layer rejections (bad +// permit, wrong allowance, reverts inside submitRequest) are a separate path +// and belong to tests against a simulated chain. +func TestPaymentAppERC20NegativePath(t *testing.T) { + if os.Getenv("CI_FLAG") != "" { + t.Skip("Skipping long running test in CI environment") + } + + t.Setenv("MANAGER_ARTIFACTS_PATH", t.TempDir()) + + suite := systemTests.NewSystemTestSuite(t, "wasmtime-payment-erc20-neg", newNetworkLogConfig(), newNetworkLogConfig()) + defer suite.Cleanup() + + wasmBytecode := buildAndLoadWasmModule(t) + + require.NoError(t, suite.StartExecutor()) + require.NoError(t, suite.StartManager()) + + appID := common.NewApplicationId(1) + timeout := 100 * time.Second + + allowedToken := ethCommon.HexToAddress("0x000000000000000000000000000000000000A11A") + disallowedToken := ethCommon.HexToAddress("0x000000000000000000000000000000000000BAD1") + recipientAddress := ethCommon.HexToAddress("0x1234567890123456789012345678901234567890") + + cryptoHelper := systemTests.NewCryptoHelper() + userA, err := cryptoHelper.GenerateUserIdentity() + require.NoError(t, err) + userB, err := cryptoHelper.GenerateUserIdentity() // registered but never deposits + require.NoError(t, err) + auditorAddress, err := cryptoHelper.GenerateUserIdentity() + require.NoError(t, err) + + // --- Deploy: only allowedToken is in the allowlist --- + deployAppWithTokens(t, suite, appID, userA, wasmBytecode, []ethCommon.Address{allowedToken}, timeout) + + tokenHex := strings.ToLower(allowedToken.Hex()) + + // --- Register all three keys (all seed-registered for simplicity) --- + executorPubKey, err := suite.GetExecutorCommunicationKey() + require.NoError(t, err) + registerSeedUser(t, suite, cryptoHelper, executorPubKey, appID, userA, timeout) + registerSeedUser(t, suite, cryptoHelper, executorPubKey, appID, userB, timeout) + registerSeedUser(t, suite, cryptoHelper, executorPubKey, appID, auditorAddress, timeout) + + // --- Baseline: userA deposits 1000 of allowedToken (succeeds) --- + baselineAmount := big.NewInt(1000) + depositToPaymentApp(t, suite, cryptoHelper, appID, commontestutil.GenerateRandomRequestID(), userA, allowedToken, baselineAmount, true) + + // --- Negative subtests. Each must fail and leave state unchanged. --- + + t.Run("deposit with disallowed token", func(t *testing.T) { + reqID := commontestutil.GenerateRandomRequestID() + req, err := cryptoHelper.CreateTokenDepositRequest(appID, reqID, userA, disallowedToken, big.NewInt(500), executorPubKey) + require.NoError(t, err) + require.NoError(t, suite.SubmitRequest(req)) + err = suite.AssertRequestCompleted(reqID, timeout) + require.Error(t, err, "deposit of disallowed token must fail") + require.Contains(t, err.Error(), "has failed") + }) + + t.Run("withdrawal exceeding balance", func(t *testing.T) { + reqID := commontestutil.GenerateRandomRequestID() + // userA has baselineAmount (1000); withdraw more than that. + tooMuch := new(big.Int).Mul(baselineAmount, big.NewInt(2)) + req, err := cryptoHelper.CreateTokenWithdrawalRequest(appID, reqID, userA, recipientAddress, allowedToken, common.ToBig(tooMuch), executorPubKey) + require.NoError(t, err) + require.NoError(t, suite.SubmitRequest(req)) + err = suite.AssertRequestCompleted(reqID, timeout) + require.Error(t, err, "withdrawal exceeding balance must fail") + require.Contains(t, err.Error(), "has failed") + }) + + t.Run("transfer exceeding balance", func(t *testing.T) { + tooMuch := new(big.Int).Mul(baselineAmount, big.NewInt(2)) + transferPayload, err := json.Marshal(map[string]interface{}{ + "type": "transfer", + "transfer": map[string]interface{}{ + "to": userB, + "tokenAddress": allowedToken, + "amount": common.ToBig(tooMuch), + }, + }) + require.NoError(t, err) + reqID := commontestutil.GenerateRandomRequestID() + req, err := cryptoHelper.CreateProcessRequest(appID, reqID, userA, transferPayload, executorPubKey) + require.NoError(t, err) + require.NoError(t, suite.SubmitRequest(req)) + err = suite.AssertRequestCompleted(reqID, timeout) + require.Error(t, err, "transfer exceeding balance must fail") + require.Contains(t, err.Error(), "has failed") + }) + + t.Run("transfer from account with no deposits", func(t *testing.T) { + // userB is registered but has never deposited — their account entry + // in state does not exist. Transfer from userB must fail with + // "Account does not exist!". + transferPayload, err := json.Marshal(map[string]interface{}{ + "type": "transfer", + "transfer": map[string]interface{}{ + "to": userA, + "tokenAddress": allowedToken, + "amount": common.ToBig(big.NewInt(100)), + }, + }) + require.NoError(t, err) + reqID := commontestutil.GenerateRandomRequestID() + req, err := cryptoHelper.CreateProcessRequest(appID, reqID, userB, transferPayload, executorPubKey) + require.NoError(t, err) + require.NoError(t, suite.SubmitRequest(req)) + err = suite.AssertRequestCompleted(reqID, timeout) + require.Error(t, err, "transfer from non-existent account must fail") + require.Contains(t, err.Error(), "has failed") + }) + + // --- Post-condition: state survived every failure. --- + // A successful deposit confirms the app is still operational. + finalDeposit := big.NewInt(50) + depositToPaymentApp(t, suite, cryptoHelper, appID, commontestutil.GenerateRandomRequestID(), userA, allowedToken, finalDeposit, true) + + // Deanonymization report: userA's balance must equal baseline + final + // deposit, nothing more and nothing less. If any of the failed subtests + // had partially mutated state, this assertion would catch it. + expected := new(big.Int).Add(baselineAmount, finalDeposit) + + accounts := fetchDeanonAccounts(t, suite, cryptoHelper, executorPubKey, appID, auditorAddress, timeout) + require.Len(t, accounts, 1, "only userA should have an account (userB never deposited)") + + for addrHex, acct := range accounts { + addr := ethCommon.HexToAddress(addrHex) + require.Equal(t, userA, addr, "unexpected account %s in report", addrHex) + + balances := acct.(map[string]interface{})["balances"].(map[string]interface{}) + balStr, ok := balances[tokenHex].(string) + require.True(t, ok, "balance for allowedToken missing on userA") + parsed, ok := new(big.Int).SetString(balStr[2:], 16) + require.True(t, ok, "failed to parse balance hex") + require.Equal(t, 0, expected.Cmp(parsed), + "userA balance after all failures + final deposit: expected %s, got %s (state was mutated by a failed operation)", + expected, parsed) + + // disallowedToken must not appear anywhere — the failed deposit + // of it must not have leaked a zero-balance entry into state. + disallowedHex := strings.ToLower(disallowedToken.Hex()) + _, leaked := balances[disallowedHex] + require.False(t, leaked, + "disallowed-token balance entry must not appear in state, but found key %s", disallowedHex) + } +} + // newNetworkLogConfig returns a zeronetwork logger config. // The suite injects the correct log-server port into RemoteLogParams before // creating the logger, so no hardcoded port is needed here. From 0de1ea1c2519bbf989068bb791b51d6699977907 Mon Sep 17 00:00:00 2001 From: Alberto Sala Date: Wed, 15 Apr 2026 18:09:50 +0200 Subject: [PATCH 03/25] Modifications after code review --- .../system_tests/payment_app_system_test.go | 85 +++++++++---------- 1 file changed, 40 insertions(+), 45 deletions(-) diff --git a/runtime/wasm-go/system_tests/payment_app_system_test.go b/runtime/wasm-go/system_tests/payment_app_system_test.go index 071f73b..69d3476 100644 --- a/runtime/wasm-go/system_tests/payment_app_system_test.go +++ b/runtime/wasm-go/system_tests/payment_app_system_test.go @@ -18,6 +18,7 @@ import ( "testing" "time" + "github.com/HorizenOfficial/vela-common-go/wasm/types" "github.com/HorizenOfficial/vela-nova/payment-app/app" "github.com/HorizenOfficial/vela/pkg/authorityservice/deployartifact" "github.com/HorizenOfficial/vela/pkg/common" @@ -386,6 +387,19 @@ func fetchDeanonAccounts(t *testing.T, suite *systemTests.SystemTestSuite, crypt return accounts } +// parseReportBalance extracts a 0x-prefixed hex balance string for tokenHex from +// a deanon report's per-account balances map and parses it into a *big.Int. +// The balance MUST be present — use this when the test expects a positive entry. +func parseReportBalance(t *testing.T, balances map[string]interface{}, tokenHex string) *big.Int { + t.Helper() + s, ok := balances[tokenHex].(string) + require.True(t, ok, "balance for token %s is missing or not a string", tokenHex) + require.True(t, strings.HasPrefix(s, "0x"), "balance not hex-prefixed: %s", s) + v, ok := new(big.Int).SetString(s[2:], 16) + require.True(t, ok, "failed to parse balance hex: %s", s) + return v +} + func TestPaymentAppFullFlow(t *testing.T) { if os.Getenv("CI_FLAG") != "" { t.Skip("Skipping long running test in CI environment") @@ -464,11 +478,7 @@ func TestPaymentAppFullFlow(t *testing.T) { require.True(t, ok, "account entry is not a map") balances, ok := acctMap["balances"].(map[string]interface{}) require.True(t, ok, "balances is not a map") - balanceStr, ok := balances[ethTokenHex].(string) - require.True(t, ok, "ETH balance is not a string for account %s", addrHex) - require.True(t, strings.HasPrefix(balanceStr, "0x"), "balance is not hex") - balance, ok := new(big.Int).SetString(balanceStr[2:], 16) - require.True(t, ok, "failed to parse balance hex") + balance := parseReportBalance(t, balances, ethTokenHex) addr := ethCommon.HexToAddress(addrHex) expected, known := expectedBalances[addr] @@ -574,11 +584,7 @@ func TestPaymentAppERC20FullFlow(t *testing.T) { require.True(t, ok, "balances is not a map") // Remaining balance must be keyed under the ERC-20 token, not ETH. - balanceStr, ok := balances[tokenHex].(string) - require.True(t, ok, "balance for token %s missing on account %s", tokenHex, addrHex) - require.True(t, strings.HasPrefix(balanceStr, "0x"), "balance is not hex-prefixed") - parsed, ok := new(big.Int).SetString(balanceStr[2:], 16) - require.True(t, ok, "failed to parse balance hex") + parsed := parseReportBalance(t, balances, tokenHex) require.Equal(t, 0, expectedRemaining.Cmp(parsed), "account %s: expected token balance %s, got %s", addrHex, expectedRemaining, parsed) @@ -664,18 +670,12 @@ func TestPaymentAppERC20MultiToken(t *testing.T) { require.True(t, ok, "balances is not a map") // tokenA: deposit - withdrawal - balanceAStr, ok := balances[tokenAHex].(string) - require.True(t, ok, "balance for tokenA (%s) missing on account %s", tokenAHex, addrHex) - parsedA, ok := new(big.Int).SetString(balanceAStr[2:], 16) - require.True(t, ok, "failed to parse tokenA balance hex") + parsedA := parseReportBalance(t, balances, tokenAHex) require.Equal(t, 0, expectedA.Cmp(parsedA), "account %s tokenA balance: expected %s, got %s", addrHex, expectedA, parsedA) // tokenB: unchanged (the withdrawal of tokenA must not have affected tokenB) - balanceBStr, ok := balances[tokenBHex].(string) - require.True(t, ok, "balance for tokenB (%s) missing on account %s", tokenBHex, addrHex) - parsedB, ok := new(big.Int).SetString(balanceBStr[2:], 16) - require.True(t, ok, "failed to parse tokenB balance hex") + parsedB := parseReportBalance(t, balances, tokenBHex) require.Equal(t, 0, expectedB.Cmp(parsedB), "account %s tokenB balance should be unchanged: expected %s, got %s", addrHex, expectedB, parsedB) } @@ -759,13 +759,14 @@ func TestPaymentAppERC20MultiUser(t *testing.T) { // --- Private transfer A -> B --- transferAmount := big.NewInt(1_200_000) - // Payload shape matches the guest's PayloadInstructions / TransferInstruction. - transferPayload, err := json.Marshal(map[string]interface{}{ - "type": "transfer", - "transfer": map[string]interface{}{ - "to": userB, - "tokenAddress": tokenAddress, - "amount": common.ToBig(transferAmount), + // Use the guest's real PayloadInstructions / TransferInstruction types so + // a field rename in app/types.go breaks compilation instead of the wire format. + transferPayload, err := json.Marshal(app.PayloadInstructions{ + Type: "transfer", + Transfer: &app.TransferInstruction{ + To: types.Address(userB), + TokenAddress: types.Address(tokenAddress), + Amount: new(types.Uint256).SetBytes(transferAmount.Bytes()), }, }) require.NoError(t, err) @@ -823,10 +824,7 @@ func TestPaymentAppERC20MultiUser(t *testing.T) { for addrHex, acct := range accounts { acctMap := acct.(map[string]interface{}) balances := acctMap["balances"].(map[string]interface{}) - balStr, ok := balances[tokenHex].(string) - require.True(t, ok, "account %s missing balance for token %s", addrHex, tokenHex) - bal, ok := new(big.Int).SetString(balStr[2:], 16) - require.True(t, ok, "failed to parse balance hex for %s", addrHex) + bal := parseReportBalance(t, balances, tokenHex) total.Add(total, bal) addr := ethCommon.HexToAddress(addrHex) @@ -943,12 +941,12 @@ func TestPaymentAppERC20NegativePath(t *testing.T) { t.Run("transfer exceeding balance", func(t *testing.T) { tooMuch := new(big.Int).Mul(baselineAmount, big.NewInt(2)) - transferPayload, err := json.Marshal(map[string]interface{}{ - "type": "transfer", - "transfer": map[string]interface{}{ - "to": userB, - "tokenAddress": allowedToken, - "amount": common.ToBig(tooMuch), + transferPayload, err := json.Marshal(app.PayloadInstructions{ + Type: "transfer", + Transfer: &app.TransferInstruction{ + To: types.Address(userB), + TokenAddress: types.Address(allowedToken), + Amount: new(types.Uint256).SetBytes(tooMuch.Bytes()), }, }) require.NoError(t, err) @@ -965,12 +963,12 @@ func TestPaymentAppERC20NegativePath(t *testing.T) { // userB is registered but has never deposited — their account entry // in state does not exist. Transfer from userB must fail with // "Account does not exist!". - transferPayload, err := json.Marshal(map[string]interface{}{ - "type": "transfer", - "transfer": map[string]interface{}{ - "to": userA, - "tokenAddress": allowedToken, - "amount": common.ToBig(big.NewInt(100)), + transferPayload, err := json.Marshal(app.PayloadInstructions{ + Type: "transfer", + Transfer: &app.TransferInstruction{ + To: types.Address(userA), + TokenAddress: types.Address(allowedToken), + Amount: new(types.Uint256).SetBytes(big.NewInt(100).Bytes()), }, }) require.NoError(t, err) @@ -1001,10 +999,7 @@ func TestPaymentAppERC20NegativePath(t *testing.T) { require.Equal(t, userA, addr, "unexpected account %s in report", addrHex) balances := acct.(map[string]interface{})["balances"].(map[string]interface{}) - balStr, ok := balances[tokenHex].(string) - require.True(t, ok, "balance for allowedToken missing on userA") - parsed, ok := new(big.Int).SetString(balStr[2:], 16) - require.True(t, ok, "failed to parse balance hex") + parsed := parseReportBalance(t, balances, tokenHex) require.Equal(t, 0, expected.Cmp(parsed), "userA balance after all failures + final deposit: expected %s, got %s (state was mutated by a failed operation)", expected, parsed) From a218c280238c8038e058b200b0121e880c1d488f Mon Sep 17 00:00:00 2001 From: Alberto Sala Date: Thu, 16 Apr 2026 13:58:40 +0200 Subject: [PATCH 04/25] WIP: full system test with real simulated chain --- runtime/wasm-go/fullstack_tests/executor.conf | 1 + .../wasm-go/fullstack_tests/helpers_test.go | 86 ++++++++++ runtime/wasm-go/fullstack_tests/manager.conf | 1 + .../payment_app_fullstack_test.go | 151 ++++++++++++++++++ runtime/wasm-go/go.mod | 56 ++++++- runtime/wasm-go/go.sum | 73 ++++++++- 6 files changed, 365 insertions(+), 3 deletions(-) create mode 120000 runtime/wasm-go/fullstack_tests/executor.conf create mode 100644 runtime/wasm-go/fullstack_tests/helpers_test.go create mode 120000 runtime/wasm-go/fullstack_tests/manager.conf create mode 100644 runtime/wasm-go/fullstack_tests/payment_app_fullstack_test.go diff --git a/runtime/wasm-go/fullstack_tests/executor.conf b/runtime/wasm-go/fullstack_tests/executor.conf new file mode 120000 index 0000000..dc10bd0 --- /dev/null +++ b/runtime/wasm-go/fullstack_tests/executor.conf @@ -0,0 +1 @@ +../system_tests/executor.conf \ No newline at end of file diff --git a/runtime/wasm-go/fullstack_tests/helpers_test.go b/runtime/wasm-go/fullstack_tests/helpers_test.go new file mode 100644 index 0000000..adba980 --- /dev/null +++ b/runtime/wasm-go/fullstack_tests/helpers_test.go @@ -0,0 +1,86 @@ +package main_test + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "mime/multipart" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "path/filepath" + "runtime" + "testing" + + "github.com/HorizenOfficial/vela/pkg/authorityservice/deployartifact" + "github.com/HorizenOfficial/vela/pkg/common" + "github.com/HorizenOfficial/vela/pkg/logger" + "github.com/HorizenOfficial/vela/pkg/testutil/fullstack" + "github.com/stretchr/testify/require" +) + +// buildAndLoadWasmModule builds the payment app WASM module and returns its bytecode. +func buildAndLoadWasmModule(t *testing.T) []byte { + t.Helper() + _, b, _, ok := runtime.Caller(0) + require.True(t, ok) + projectRoot := filepath.Join(filepath.Dir(b), "../..") + appDir := filepath.Join(projectRoot, "wasm-go") + + cmd := exec.Command("make", "build") + cmd.Dir = appDir + output, err := cmd.CombinedOutput() + require.NoError(t, err, "failed to build wasm module: %s", string(output)) + + wasmPath := filepath.Join(appDir, "build", "payment_app.wasm") + wasmBytecode, err := os.ReadFile(wasmPath) + require.NoError(t, err) + require.NotEmpty(t, wasmBytecode) + + return wasmBytecode +} + +// uploadArtifactAndBuildDescriptorPayload uploads a WASM artifact to the local +// artifact store and returns a deploy descriptor payload (JSON) that references it. +func uploadArtifactAndBuildDescriptorPayload(t *testing.T, suite *fullstack.FullStackSystemTestSuite, wasmBytecode []byte) []byte { + t.Helper() + + store, err := deployartifact.NewStore(suite.GetArtifactsPath()) + require.NoError(t, err) + uploadAPI := deployartifact.NewAPI(store, 50, logger.NewLogger(&logger.Config{Kind: "zerolog", Console: false})) + + var body bytes.Buffer + writer := multipart.NewWriter(&body) + fileWriter, err := writer.CreateFormFile("wasm", "app.wasm") + require.NoError(t, err) + _, err = fileWriter.Write(wasmBytecode) + require.NoError(t, err) + require.NoError(t, writer.Close()) + + req := httptest.NewRequest(http.MethodPost, "/deploy/upload", &body) + req.Header.Set("Content-Type", writer.FormDataContentType()) + rr := httptest.NewRecorder() + uploadAPI.HandleUpload(rr, req) + require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) + + var uploadResp deployartifact.UploadResponse + require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &uploadResp)) + + localSum := sha256.Sum256(wasmBytecode) + localSHA := hex.EncodeToString(localSum[:]) + localArtifactID, err := common.BuildArtifactID(localSHA) + require.NoError(t, err) + require.Equal(t, localSHA, uploadResp.WasmSHA256) + require.Equal(t, localArtifactID, uploadResp.ArtifactID) + + descriptor := common.DeployDescriptor{ + Mode: common.DeployModeArtifactRef, + ArtifactID: uploadResp.ArtifactID, + WasmSHA256: uploadResp.WasmSHA256, + } + payload, err := json.Marshal(descriptor) + require.NoError(t, err) + return payload +} diff --git a/runtime/wasm-go/fullstack_tests/manager.conf b/runtime/wasm-go/fullstack_tests/manager.conf new file mode 120000 index 0000000..dc091cb --- /dev/null +++ b/runtime/wasm-go/fullstack_tests/manager.conf @@ -0,0 +1 @@ +../system_tests/manager.conf \ No newline at end of file diff --git a/runtime/wasm-go/fullstack_tests/payment_app_fullstack_test.go b/runtime/wasm-go/fullstack_tests/payment_app_fullstack_test.go new file mode 100644 index 0000000..a10ac1c --- /dev/null +++ b/runtime/wasm-go/fullstack_tests/payment_app_fullstack_test.go @@ -0,0 +1,151 @@ +package main_test + +import ( + "encoding/json" + "math/big" + "os" + "testing" + "time" + + "github.com/HorizenOfficial/vela/pkg/common" + cryptotypes "github.com/HorizenOfficial/vela/pkg/common/crypto" + commontestutil "github.com/HorizenOfficial/vela/pkg/common/testutil" + "github.com/HorizenOfficial/vela/pkg/executor" + "github.com/HorizenOfficial/vela/pkg/logger" + systemTests "github.com/HorizenOfficial/vela/pkg/testutil" + "github.com/HorizenOfficial/vela/pkg/testutil/fullstack" + ethCommon "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +// createFullstackUser creates a funded on-chain account and registers its +// signing key with the CryptoHelper so that seed computation and payload +// encryption work correctly. +func createFullstackUser(t *testing.T, suite *fullstack.FullStackSystemTestSuite, cryptoHelper *systemTests.CryptoHelper) (ethCommon.Address, error) { + t.Helper() + addr, signingKey, err := suite.CreateFundedAccount() + if err != nil { + return ethCommon.Address{}, err + } + cryptoHelper.RegisterUserSigningKey(addr, signingKey) + return addr, nil +} + +// registerSeedUser generates a P521 key, submits an AssociateKey request with +// an encrypted seed, and waits for completion. +func registerSeedUser(t *testing.T, suite *fullstack.FullStackSystemTestSuite, cryptoHelper *systemTests.CryptoHelper, executorPubKey *cryptotypes.PublicKeyP521, appID common.ApplicationIdType, user ethCommon.Address, timeout time.Duration) { + t.Helper() + userKey, err := cryptoHelper.GenerateUserKey(user) + require.NoError(t, err) + reqID := commontestutil.GenerateRandomRequestID() + req, err := cryptoHelper.CreateAssociateKeyRequest(appID, reqID, user, userKey.PublicKey(), executorPubKey) + require.NoError(t, err) + require.NoError(t, suite.SubmitRequest(req)) + require.NoError(t, suite.AssertRequestCompleted(req.RequestID, timeout)) +} + +// TestFullStackDeployAndDeposit is the simplest fullstack smoke test: +// deploy an app on the real simulated chain, register a user, deposit ETH, +// and verify the event is received. This proves the entire stack works +// end-to-end: on-chain submission → manager polls chain → executor processes +// WASM → manager submits state update on-chain → events observed in test. +func TestFullStackDeployAndDeposit(t *testing.T) { + if os.Getenv("CI_FLAG") != "" { + t.Skip("Skipping fullstack test in CI environment") + } + + t.Setenv("MANAGER_ARTIFACTS_PATH", t.TempDir()) + + suite := fullstack.NewFullStackSystemTestSuite(t, "wasmtime-payment", newNetworkLogConfig(), newNetworkLogConfig()) + defer suite.Cleanup() + + wasmBytecode := buildAndLoadWasmModule(t) + + require.NoError(t, suite.StartExecutor()) + require.NoError(t, suite.StartManager()) + + timeout := 100 * time.Second + + cryptoHelper := systemTests.NewCryptoHelper() + + // Create funded on-chain accounts for user and auditor + userAddress, err := createFullstackUser(t, suite, cryptoHelper) + require.NoError(t, err) + + // Deploy the app — appID is assigned by the contract. + // The ProcessorEndpoint contract requires the sender to have the deployer role, + // so we use the pre-authorized deployer account from the simulated chain. + deployDescriptor := uploadArtifactAndBuildDescriptorPayload(t, suite, wasmBytecode) + deployReq := &common.Request{ + RequestType: common.Deploy, + Payload: deployDescriptor, + Sender: suite.GetDeployerAddress(), + Timestamp: common.ToBig(new(big.Int).SetInt64(time.Now().Unix())), + AssetAmount: common.NewBig(0), + MaxFeeValue: common.NewBig(100), + } + require.NoError(t, suite.SubmitRequest(deployReq)) + appID := deployReq.ApplicationID + t.Logf("Contract assigned appID: %d", appID) + + _, err = suite.WaitForAppStateInDB(appID, timeout) + require.NoError(t, err) + _, err = suite.WaitForAppStateInBlockchain(appID, timeout) + require.NoError(t, err) + + // Get executor public key for encryption + executorPubKey, err := suite.GetExecutorCommunicationKey() + require.NoError(t, err) + + // Register user key (seed-registered) + registerSeedUser(t, suite, cryptoHelper, executorPubKey, appID, userAddress, timeout) + + // Deposit 2 ETH + depositAmount := big.NewInt(2_000_000_000_000_000_000) + depositReq, err := cryptoHelper.CreateTokenDepositRequest(appID, commontestutil.GenerateRandomRequestID(), userAddress, ethCommon.Address{}, depositAmount, executorPubKey) + require.NoError(t, err) + require.NoError(t, suite.SubmitRequest(depositReq)) + require.NoError(t, suite.AssertRequestCompleted(depositReq.RequestID, timeout)) + + // Verify deposit event received + userSeed, err := cryptoHelper.ComputeSeed(userAddress) + require.NoError(t, err) + depositEvent, err := suite.WaitForEventBySubtypes(userAddress, executor.AllSubtypes(userSeed, executor.DefaultSubtypeN), timeout) + require.NoError(t, err) + + // Decrypt and validate event + decryptedData, err := cryptoHelper.DecryptEvent(userAddress, depositEvent, executorPubKey) + require.NoError(t, err) + var eventData struct { + Type string `json:"type"` + Amount *common.Big `json:"amount"` + Balance *common.Big `json:"balance"` + } + require.NoError(t, json.Unmarshal(decryptedData, &eventData)) + require.Equal(t, "deposit", eventData.Type) + require.Equal(t, 0, depositAmount.Cmp(eventData.Amount.ToInt()), + "deposit event amount: expected %s, got %s", depositAmount, eventData.Amount.ToInt()) + + // Verify signature on the update payload + executorSigningKey, err := suite.GetExecutorSigningKey() + require.NoError(t, err) + payload, err := suite.GetRequestUpdatePayload(depositReq.RequestID) + require.NoError(t, err) + require.NoError(t, cryptoHelper.ValidateUpdatePayloadSignature(payload, executorSigningKey)) + + t.Log("Fullstack deploy + deposit test passed") +} + +// --- Shared helpers (same as system_tests, adapted for fullstack suite) --- + +func newNetworkLogConfig() *logger.Config { + return &logger.Config{ + Kind: "zeronetwork", + ConsoleColor: false, + Console: true, + ConsoleLevel: "trace", + FileLevel: "trace", + RemoteLogNetwork: "tcp", + NetworkLevel: "trace", + } +} diff --git a/runtime/wasm-go/go.mod b/runtime/wasm-go/go.mod index 38af517..fa4a34e 100644 --- a/runtime/wasm-go/go.mod +++ b/runtime/wasm-go/go.mod @@ -7,7 +7,7 @@ go 1.24.0 toolchain go1.24.3 // Can be useful for local developments -//replace github.com/HorizenOfficial/vela => ../../../vela +replace github.com/HorizenOfficial/vela => ../../../vela //replace github.com/HorizenOfficial/vela-common-go => ../../../vela-common-go @@ -19,9 +19,11 @@ require ( ) require ( + github.com/DataDog/zstd v1.4.5 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect github.com/StackExchange/wmi v1.2.1 // indirect + github.com/VictoriaMetrics/fastcache v1.13.0 // indirect github.com/aws/aws-sdk-go-v2 v1.41.1 // indirect github.com/aws/aws-sdk-go-v2/config v1.29.14 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect @@ -36,48 +38,100 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect github.com/aws/smithy-go v1.24.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.20.0 // indirect github.com/bytecodealliance/wasmtime-go v1.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cockroachdb/errors v1.11.3 // indirect + github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce // indirect + github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect + github.com/cockroachdb/pebble v1.1.5 // indirect + github.com/cockroachdb/redact v1.1.5 // indirect + github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect github.com/consensys/gnark-crypto v0.18.1 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dchest/siphash v1.2.3 // indirect github.com/deckarep/golang-set/v2 v2.6.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect + github.com/emicklei/dot v1.6.2 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect + github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab // indirect + github.com/ferranbt/fastssz v0.1.4 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/fxamacker/cbor/v2 v2.2.0 // indirect + github.com/getsentry/sentry-go v0.27.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect + github.com/gofrs/flock v0.12.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect + github.com/grafana/pyroscope-go v1.2.7 // indirect + github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect + github.com/hashicorp/go-bexpr v0.1.10 // indirect github.com/hf/nsm v0.0.0-20220930140112-cd181bd646b9 // indirect + github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db // indirect + github.com/holiman/bloomfilter/v2 v2.0.3 // indirect github.com/holiman/uint256 v1.3.2 // indirect + github.com/huin/goupnp v1.3.0 // indirect + github.com/jackpal/go-nat-pmp v1.0.2 // indirect + github.com/klauspost/compress v1.17.8 // indirect + github.com/klauspost/cpuid/v2 v2.0.9 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/magiconair/properties v1.8.10 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mdlayher/socket v0.4.1 // indirect github.com/mdlayher/vsock v1.2.1 // indirect + github.com/minio/sha256-simd v1.0.0 // indirect + github.com/mitchellh/mapstructure v1.4.1 // indirect + github.com/mitchellh/pointerstructure v1.2.0 // indirect + github.com/pion/dtls/v2 v2.2.7 // indirect + github.com/pion/logging v0.2.2 // indirect + github.com/pion/stun/v2 v2.0.0 // indirect + github.com/pion/transport/v2 v2.2.1 // indirect + github.com/pion/transport/v3 v3.0.1 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.15.0 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.42.0 // indirect + github.com/prometheus/procfs v0.9.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/rs/cors v1.7.0 // indirect github.com/rs/zerolog v1.34.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/supranational/blst v0.3.16 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect + github.com/urfave/cli/v2 v2.27.5 // indirect github.com/x448/float16 v0.8.4 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.40.0 // indirect go.opentelemetry.io/otel/metric v1.40.0 // indirect go.opentelemetry.io/otel/trace v1.40.0 // indirect golang.org/x/crypto v0.44.0 // indirect + golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/time v0.9.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/runtime/wasm-go/go.sum b/runtime/wasm-go/go.sum index 3e200f4..0a952ce 100644 --- a/runtime/wasm-go/go.sum +++ b/runtime/wasm-go/go.sum @@ -1,7 +1,5 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= -github.com/HorizenOfficial/vela v0.0.28 h1:xmXA9PMUV64msBb84mi0VwXQuqMJXPX2wVPBCgue8c8= -github.com/HorizenOfficial/vela v0.0.28/go.mod h1:E/SXS/mBVYakoFwzEmMMw8w5BUxIv+geUhwB5J0JRcE= github.com/HorizenOfficial/vela-common-go v0.0.28 h1:iVeJMUYIDJ83eAzoS0+gMQRjnQj9/yKv9cp2ev3PFNg= github.com/HorizenOfficial/vela-common-go v0.0.28/go.mod h1:VGEDebTDUnwrnMycUHQtm9WBi0DpRcYjsetIU4KbRmc= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -12,6 +10,8 @@ github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDO github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM= @@ -50,6 +50,8 @@ github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= +github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= @@ -69,6 +71,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc= github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= @@ -105,6 +109,8 @@ github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -121,6 +127,7 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69 github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -170,8 +177,11 @@ github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7 github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -217,6 +227,8 @@ github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsq github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= @@ -227,6 +239,7 @@ github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -239,8 +252,11 @@ github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/prysmaticlabs/gohashtree v0.0.4-beta h1:H/EbCuXPeTV3lpKeXGPpEV9gsUpkqOOVnWapUyeWro4= +github.com/prysmaticlabs/gohashtree v0.0.4-beta/go.mod h1:BFdtALS+Ffhg3lGQIHv9HDWuHS8cTvHZzrHWxwOtGOs= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= @@ -252,6 +268,16 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= @@ -268,7 +294,9 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= @@ -279,27 +307,47 @@ go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ7 go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -313,18 +361,34 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= @@ -332,7 +396,11 @@ golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210105210202-9ed45478a130/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -358,5 +426,6 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From dae8adfa307752ba8baeca0f5590a55e55e33636 Mon Sep 17 00:00:00 2001 From: Alberto Sala Date: Thu, 16 Apr 2026 14:35:16 +0200 Subject: [PATCH 05/25] Aded subgraph checks in full stack test --- .../payment_app_fullstack_test.go | 55 +++++++++++++++++-- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/runtime/wasm-go/fullstack_tests/payment_app_fullstack_test.go b/runtime/wasm-go/fullstack_tests/payment_app_fullstack_test.go index a10ac1c..d4cf783 100644 --- a/runtime/wasm-go/fullstack_tests/payment_app_fullstack_test.go +++ b/runtime/wasm-go/fullstack_tests/payment_app_fullstack_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + velacommon "github.com/HorizenOfficial/vela-common-go/common" "github.com/HorizenOfficial/vela/pkg/common" cryptotypes "github.com/HorizenOfficial/vela/pkg/common/crypto" commontestutil "github.com/HorizenOfficial/vela/pkg/common/testutil" @@ -44,11 +45,13 @@ func registerSeedUser(t *testing.T, suite *fullstack.FullStackSystemTestSuite, c require.NoError(t, suite.AssertRequestCompleted(req.RequestID, timeout)) } -// TestFullStackDeployAndDeposit is the simplest fullstack smoke test: +// TestFullStackDeployAndDeposit is the fullstack smoke test: // deploy an app on the real simulated chain, register a user, deposit ETH, -// and verify the event is received. This proves the entire stack works +// verify the event is received, and query the in-process subgraph for +// request completions and user events. This proves the entire stack works // end-to-end: on-chain submission → manager polls chain → executor processes -// WASM → manager submits state update on-chain → events observed in test. +// WASM → manager submits state update on-chain → events and subgraph data +// observed in test. func TestFullStackDeployAndDeposit(t *testing.T) { if os.Getenv("CI_FLAG") != "" { t.Skip("Skipping fullstack test in CI environment") @@ -68,7 +71,7 @@ func TestFullStackDeployAndDeposit(t *testing.T) { cryptoHelper := systemTests.NewCryptoHelper() - // Create funded on-chain accounts for user and auditor + // Create funded on-chain account for the user userAddress, err := createFullstackUser(t, suite, cryptoHelper) require.NoError(t, err) @@ -133,7 +136,49 @@ func TestFullStackDeployAndDeposit(t *testing.T) { require.NoError(t, err) require.NoError(t, cryptoHelper.ValidateUpdatePayloadSignature(payload, executorSigningKey)) - t.Log("Fullstack deploy + deposit test passed") + // --- In-process subgraph verification --- + // Query the subgraph for the completed deposit request + sgClient := suite.GetSubgraphClient() + + depositCompleted, err := sgClient.GetRequestCompletedByID(t.Context(), depositReq.RequestID) + require.NoError(t, err) + require.NotNil(t, depositCompleted, "subgraph should have the deposit request completion") + require.Equal(t, velacommon.RequestResultOK, depositCompleted.Status) + require.Equal(t, appID, depositCompleted.ApplicationID) + t.Logf("Subgraph: deposit request completed at block %d", depositCompleted.BlockNumber) + + // Query the subgraph for the deploy request completion + deployCompleted, err := sgClient.GetDeployRequestCompletedByID(t.Context(), deployReq.RequestID) + require.NoError(t, err) + require.NotNil(t, deployCompleted, "subgraph should have the deploy request completion") + require.Equal(t, velacommon.RequestResultOK, deployCompleted.Status) + + // Query user events from the subgraph — the deposit should have produced an event + // with the hashed subtype. Query with all subtypes for this user's seed. + allSubtypes := executor.AllSubtypes(userSeed, executor.DefaultSubtypeN) + sgEvents, err := sgClient.GetUserEventsBySubTypes(t.Context(), appID, allSubtypes, 10, nil) + require.NoError(t, err) + require.NotEmpty(t, sgEvents, "subgraph should have at least one user event after deposit") + t.Logf("Subgraph: found %d user events for the deposit", len(sgEvents)) + + // The most recent event should be the deposit event — decrypt and verify + sgDecrypted, err := cryptoHelper.DecryptEvent(userAddress, &common.Event{ + ApplicationID: sgEvents[0].ApplicationID, + UserID: userAddress, + EventSubType: sgEvents[0].EventSubType, + EncryptedData: sgEvents[0].EncryptedData, + }, executorPubKey) + require.NoError(t, err) + var sgEventData struct { + Type string `json:"type"` + Amount *common.Big `json:"amount"` + } + require.NoError(t, json.Unmarshal(sgDecrypted, &sgEventData)) + require.Equal(t, "deposit", sgEventData.Type) + require.Equal(t, 0, depositAmount.Cmp(sgEventData.Amount.ToInt()), + "subgraph event amount: expected %s, got %s", depositAmount, sgEventData.Amount.ToInt()) + + t.Log("Fullstack deploy + deposit + subgraph verification passed") } // --- Shared helpers (same as system_tests, adapted for fullstack suite) --- From c53da3f49c30e9f14776473a26fbf1d05d8280ea Mon Sep 17 00:00:00 2001 From: Alberto Sala Date: Mon, 20 Apr 2026 16:17:14 +0200 Subject: [PATCH 06/25] Modifications useful for adding a wallet driver usable in full stack system test flows --- wallet/cmd/claimpendingpayments.go | 83 ++++++------ wallet/cmd/constants.go | 27 ++++ wallet/cmd/deployapp.go | 7 +- wallet/cmd/deployapp_test.go | 22 ++-- wallet/cmd/deposit.go | 117 +++++++---------- wallet/cmd/getprivatebalance.go | 125 +++++++++--------- wallet/cmd/registeruser.go | 105 +++++++-------- wallet/cmd/withdraw.go | 161 ++++++++++------------- wallet/go.mod | 25 +++- wallet/go.sum | 55 +++++++- wallet/testutil/wallet_driver.go | 180 ++++++++++++++++++++++++++ wallet/testutil/wallet_driver_test.go | 75 +++++++++++ wallet/testutil/wrappers.go | 141 ++++++++++++++++++++ 13 files changed, 785 insertions(+), 338 deletions(-) create mode 100644 wallet/testutil/wallet_driver.go create mode 100644 wallet/testutil/wallet_driver_test.go create mode 100644 wallet/testutil/wrappers.go diff --git a/wallet/cmd/claimpendingpayments.go b/wallet/cmd/claimpendingpayments.go index f40fb99..8b32d62 100644 --- a/wallet/cmd/claimpendingpayments.go +++ b/wallet/cmd/claimpendingpayments.go @@ -5,9 +5,9 @@ import ( "fmt" "math/big" - ethCommon "github.com/ethereum/go-ethereum/common" "github.com/HorizenOfficial/vela-nova/wallet/app" "github.com/HorizenOfficial/vela/pkg/blockchain" + ethCommon "github.com/ethereum/go-ethereum/common" "github.com/spf13/cobra" ) @@ -22,57 +22,52 @@ func NewClaimPendingPaymentsCommand(config *app.Config, blockchainClient blockch } } +// Exec runs the claimpendingpayments flow outside of the Cobra wrapper. +// Exported so test drivers can invoke it directly after setting flag-bound +// struct fields. +func (c *ClaimPendingPaymentsCommand) Exec(ctx context.Context) error { + if c.Config.KeySecp == nil { + return fmt.Errorf("Secp256k1 key not found in the wallet") + } + + tokenInfo, err := c.Config.Tokens.ResolveToken(c.token) + if err != nil { + return err + } + + if err := c.InitChainClient(ctx); err != nil { + return fmt.Errorf("connecting to rpc node: %w", err) + } + defer c.CloseClient() + + address := ethCommon.HexToAddress(c.Config.KeySecp.PublicKey().Address()) + + amount, err := c.BlockchainClient.GetPendingClaims(ctx, tokenInfo.Address, address) + if err != nil { + return fmt.Errorf("retrieving pending claims: %w", err) + } + if amount.Cmp(big.NewInt(0)) == 0 { + fmt.Printf("Nothing to claim for %s\n", tokenInfo.Symbol) + return nil + } + + fmt.Printf("Claiming %s...\n", c.Config.Tokens.FormatAmount(amount, tokenInfo)) + if err := c.BlockchainClient.Claim(ctx, tokenInfo.Address, address); err != nil { + return fmt.Errorf("claiming pending payments: %w", err) + } + fmt.Println("Pending payments claimed successfully") + return nil +} + func (c *ClaimPendingPaymentsCommand) Command() *cobra.Command { cmd := &cobra.Command{ Use: "claimpendingpayments", Short: `claim pending payments to your public address (amounts coming from fee refunds or withdrawals not yet claimed)`, Long: `claim pending payments from the ProcessorEndpoint contract to your public address`, Run: func(cmd *cobra.Command, args []string) { - if c.Config.KeySecp == nil { - fmt.Println("Error: Secp256k1 key not found in the wallet") - return - } - - // Resolve token - tokenInfo, err := c.Config.Tokens.ResolveToken(c.token) - if err != nil { + if err := c.Exec(resolveContext(cmd)); err != nil { fmt.Printf("Error: %v\n", err) - return - } - - ctx := context.Background() - if cmd != nil && cmd.Context() != nil { - ctx = cmd.Context() - } - if c.BlockchainClient == nil { - if err := c.InitChainClient(ctx); err != nil { - fmt.Printf("Error connecting to rpc node: %v\n", err) - return - } - } - defer c.CloseClient() - - address := ethCommon.HexToAddress(c.Config.KeySecp.PublicKey().Address()) - - amount, err := c.BlockchainClient.GetPendingClaims(ctx, tokenInfo.Address, address) - if err != nil { - fmt.Printf("Error retrieving pending claims: %v\n", err) - return } - if amount.Cmp(big.NewInt(0)) == 0 { - fmt.Printf("Nothing to claim for %s\n", tokenInfo.Symbol) - return - } - - fmt.Printf("Claiming %s...\n", c.Config.Tokens.FormatAmount(amount, tokenInfo)) - - err = c.BlockchainClient.Claim(ctx, tokenInfo.Address, address) - if err != nil { - fmt.Printf("Error claiming pending payments: %v\n", err) - return - } - - fmt.Println("Pending payments claimed successfully") }, } cmd.Flags().StringVarP(&c.token, "token", "k", "", "Token symbol or address (default: ETH)") diff --git a/wallet/cmd/constants.go b/wallet/cmd/constants.go index 7157254..b6ff6aa 100644 --- a/wallet/cmd/constants.go +++ b/wallet/cmd/constants.go @@ -1,7 +1,12 @@ package cmd import ( + "context" + "math/big" + + "github.com/HorizenOfficial/vela-nova/wallet/app" ethCommon "github.com/ethereum/go-ethereum/common" + "github.com/spf13/cobra" ) // ETH_TOKEN is the sentinel address representing native ETH. @@ -10,3 +15,25 @@ var ETH_TOKEN = ethCommon.Address{} const PROTOCOL_VERSION uint8 = 0 const BLOCK_BATCH_SIZE = 100000 + +// resolveContext returns cmd.Context() when set, falling back to +// context.Background() otherwise. Cobra leaves c.ctx nil unless the caller +// went through ExecuteContext, so every command's Run handler needs this +// defense before passing ctx to downstream code that may not tolerate nil. +func resolveContext(cmd *cobra.Command) context.Context { + if ctx := cmd.Context(); ctx != nil { + return ctx + } + return context.Background() +} + +// parseAssetAmount parses a user-supplied amount into a *big.Int, routing to +// the ETH parser for native ETH and to the token registry's ERC-20 parser +// otherwise. Keeps the ETH/ERC-20 branching in one place for commands that +// accept an asset amount (deposit, withdraw). +func parseAssetAmount(tokens *app.TokenRegistry, info *app.TokenInfo, value string) (*big.Int, error) { + if info.Address == ETH_TOKEN { + return app.ParseEtherValue(value) + } + return tokens.ParseAmount(value, info) +} diff --git a/wallet/cmd/deployapp.go b/wallet/cmd/deployapp.go index 6b85699..9eb4bf7 100644 --- a/wallet/cmd/deployapp.go +++ b/wallet/cmd/deployapp.go @@ -43,7 +43,7 @@ func (c *DeployAppCommand) Command() *cobra.Command { Short: `Uploads a wasm artifact and triggers app deployment`, Long: `Uploads a wasm artifact to the authority service and submits a deploy request with an artifact descriptor payload.`, Run: func(cmd *cobra.Command, args []string) { - if err := c.run(context.Background()); err != nil { + if err := c.Exec(context.Background()); err != nil { fmt.Printf("Error: %v\n", err) } }, @@ -57,7 +57,10 @@ func (c *DeployAppCommand) Command() *cobra.Command { return cmd } -func (c *DeployAppCommand) run(ctx context.Context) error { +// Exec runs the deploy flow outside of the Cobra wrapper. Exported so test +// drivers (wallet/testutil) can invoke it directly after setting flag-bound +// struct fields via cmd.Flags().Set(...). +func (c *DeployAppCommand) Exec(ctx context.Context) error { maxFeeValue, err := app.ParseEtherValue(c.maxFeeValue) if err != nil { return fmt.Errorf("invalid max fee amount: %w", err) diff --git a/wallet/cmd/deployapp_test.go b/wallet/cmd/deployapp_test.go index a69725b..f59e01e 100644 --- a/wallet/cmd/deployapp_test.go +++ b/wallet/cmd/deployapp_test.go @@ -140,7 +140,7 @@ func TestDeployAppCommand_Success(t *testing.T) { cmd.wasmPath = wasmPath cmd.maxFeeValue = "100 wei" - err := cmd.run(context.Background()) + err := cmd.Exec(context.Background()) require.NoError(t, err) require.NotEmpty(t, mockBC.deployPayload) @@ -165,7 +165,7 @@ func TestDeployAppCommand_FailsWithoutArtifactServiceURL(t *testing.T) { cmd.wasmPath = wasmPath cmd.maxFeeValue = "100 wei" - err := cmd.run(context.Background()) + err := cmd.Exec(context.Background()) require.ErrorContains(t, err, "authority service URL is required") } @@ -194,7 +194,7 @@ func TestDeployAppCommand_FailsOnUploadHashMismatch(t *testing.T) { cmd.wasmPath = wasmPath cmd.maxFeeValue = "100 wei" - err := cmd.run(context.Background()) + err := cmd.Exec(context.Background()) require.ErrorContains(t, err, "deploy upload hash mismatch") require.Empty(t, mockBC.deployPayload) @@ -230,7 +230,7 @@ func TestDeployAppCommand_OverwritesExistingApplicationID(t *testing.T) { cmd.wasmPath = wasmPath cmd.maxFeeValue = "100 wei" - err := cmd.run(context.Background()) + err := cmd.Exec(context.Background()) require.NoError(t, err) // Read the raw file — old value should be commented out @@ -289,7 +289,7 @@ func TestDeployThenRestart_LoadedConfigUsesAssignedApplicationID(t *testing.T) { deployCmd.wasmPath = wasmPath deployCmd.maxFeeValue = "100 wei" - err := deployCmd.run(context.Background()) + err := deployCmd.Exec(context.Background()) require.NoError(t, err) // Phase 2: Simulate wallet restart — load config fresh from the file @@ -362,7 +362,7 @@ func TestDeployAppCommand_UnauthorizedDeployerReverts(t *testing.T) { cmd.wasmPath = wasmPath cmd.maxFeeValue = "100 wei" - err := cmd.run(context.Background()) + err := cmd.Exec(context.Background()) require.Error(t, err) require.Contains(t, err.Error(), "DeployerNotAllowed", "contract should reject deploy from an account without DEPLOYER_ROLE") @@ -421,7 +421,7 @@ func TestDeployAppCommand_WithAllowedTokens(t *testing.T) { // silently dropped since guest always allows it). cmd.allowedTokens = []string{"MOCK", mockAddr, "ETH"} - require.NoError(t, cmd.run(context.Background())) + require.NoError(t, cmd.Exec(context.Background())) // Decode the submitted deploy payload and inspect ConstructorParams var payload struct { @@ -485,7 +485,7 @@ func TestDeployAppCommand_WithAllowedTokens_HexAddressOnly(t *testing.T) { // Pass ONLY the raw hex address (no symbol) cmd.allowedTokens = []string{mockAddrChecksum} - require.NoError(t, cmd.run(context.Background())) + require.NoError(t, cmd.Exec(context.Background())) var payload struct { ConstructorParams json.RawMessage `json:"constructorParams"` @@ -542,7 +542,7 @@ func TestDeployAppCommand_WithAllowedTokens_MultipleDistinctTokens(t *testing.T) cmd.maxFeeValue = "100 wei" cmd.allowedTokens = []string{"MOCK", "USDC"} - require.NoError(t, cmd.run(context.Background())) + require.NoError(t, cmd.Exec(context.Background())) var payload struct { ConstructorParams json.RawMessage `json:"constructorParams"` @@ -590,7 +590,7 @@ func TestDeployAppCommand_NoAllowedTokens_OmitsConstructorParams(t *testing.T) { cmd.maxFeeValue = "100 wei" // allowedTokens left as zero value (nil) — simulating no flag given - require.NoError(t, cmd.run(context.Background())) + require.NoError(t, cmd.Exec(context.Background())) // Decode into a map so we can check for field absence (not presence-with-zero-value) var payload map[string]any @@ -634,7 +634,7 @@ func TestDeployAppCommand_WithAllowedTokens_UnknownSymbol(t *testing.T) { cmd.maxFeeValue = "100 wei" cmd.allowedTokens = []string{"UNKNOWN"} - err = cmd.run(context.Background()) + err = cmd.Exec(context.Background()) require.ErrorContains(t, err, "--allowed-tokens") require.Empty(t, mockBC.deployPayload, "no request should be submitted when resolution fails") } diff --git a/wallet/cmd/deposit.go b/wallet/cmd/deposit.go index 2fc4560..d0d10c8 100644 --- a/wallet/cmd/deposit.go +++ b/wallet/cmd/deposit.go @@ -3,7 +3,6 @@ package cmd import ( "context" "fmt" - "math/big" "github.com/HorizenOfficial/vela-nova/wallet/app" "github.com/HorizenOfficial/vela/pkg/blockchain" @@ -19,91 +18,71 @@ type DepositCommand struct { } func NewDepositCommand(config *app.Config, blockchainClient blockchain.Client) *DepositCommand { - cmd := &DepositCommand{ + return &DepositCommand{ ChainCommand: app.NewChainCommand(config, blockchainClient), } - - return cmd } -func (c *DepositCommand) Command() *cobra.Command { - cmd := &cobra.Command{ - Use: "deposit", - Short: `deposit funds into the Vela system`, - Long: `deposit funds into the Vela system`, - Run: func(cmd *cobra.Command, args []string) { - if err := c.RequireApplicationID(); err != nil { - fmt.Printf("Error: %v\n", err) - return - } +// Exec runs the deposit flow outside of the Cobra wrapper. Exported so test +// drivers can invoke it directly after setting flag-bound struct fields. +func (c *DepositCommand) Exec(ctx context.Context) error { + if err := c.RequireApplicationID(); err != nil { + return err + } - // Resolve token - tokenInfo, err := c.Config.Tokens.ResolveToken(c.token) - if err != nil { - fmt.Printf("Error: %v\n", err) - return - } + tokenInfo, err := c.Config.Tokens.ResolveToken(c.token) + if err != nil { + return err + } - // Parse amount: ETH uses ParseEtherValue (supports unit suffixes), - // ERC-20 uses token-aware parsing with the correct decimals. - var amount *big.Int - if tokenInfo.Address == ETH_TOKEN { - amount, err = app.ParseEtherValue(c.depositAmount) - } else { - amount, err = c.Config.Tokens.ParseAmount(c.depositAmount, tokenInfo) - } - if err != nil { - fmt.Printf("Error: invalid amount: %v\n", err) - return - } + amount, err := parseAssetAmount(c.Config.Tokens, tokenInfo, c.depositAmount) + if err != nil { + return fmt.Errorf("invalid amount: %w", err) + } - maxFeeValue, err := app.ParseEtherValue(c.maxFeeValue) - if err != nil { - fmt.Printf("Error: invalid max fee amount: %v\n", err) - return - } + maxFeeValue, err := app.ParseEtherValue(c.maxFeeValue) + if err != nil { + return fmt.Errorf("invalid max fee amount: %w", err) + } - ctx := context.Background() - if cmd != nil && cmd.Context() != nil { - ctx = cmd.Context() - } - if c.BlockchainClient == nil { - if err := c.InitChainClient(ctx); err != nil { - fmt.Printf("Error connecting to rpc node: %v\n", err) - return - } - } - defer c.CloseClient() + if err := c.InitChainClient(ctx); err != nil { + return fmt.Errorf("connecting to rpc node: %w", err) + } + defer c.CloseClient() - // Print reminder for ERC-20 deposits - if tokenInfo.Address != ETH_TOKEN { - fmt.Printf("Note: ensure you have approved the ProcessorEndpoint contract to spend %s %s\n", - c.depositAmount, tokenInfo.Symbol) - } + // Print reminder for ERC-20 deposits + if tokenInfo.Address != ETH_TOKEN { + fmt.Printf("Note: ensure you have approved the ProcessorEndpoint contract to spend %s %s\n", + c.depositAmount, tokenInfo.Symbol) + } - var payload []byte - requestType := common.Process - requestID, _, err := c.BlockchainClient.SubmitRequest(ctx, PROTOCOL_VERSION, c.Config.ApplicationID, requestType, payload, tokenInfo.Address, amount, maxFeeValue) - if err != nil { - fmt.Printf("Error sending request to deposit %s %s: %v\n", c.depositAmount, tokenInfo.Symbol, err) - return - } + var payload []byte + requestID, _, err := c.BlockchainClient.SubmitRequest(ctx, PROTOCOL_VERSION, c.Config.ApplicationID, common.Process, payload, tokenInfo.Address, amount, maxFeeValue) + if err != nil { + return fmt.Errorf("sending request to deposit %s %s: %w", c.depositAmount, tokenInfo.Symbol, err) + } - fmt.Println("Waiting for confirmation from Vela") + fmt.Println("Waiting for confirmation from Vela") + if err := c.WaitForRequestCompleted(requestID, ctx); err != nil { + return fmt.Errorf("Deposit failed: %w", err) + } + fmt.Println("Deposit completed successfully") + return nil +} - err = c.WaitForRequestCompleted(requestID, ctx) - if err != nil { - fmt.Printf("Deposit failed: %v\n", err) - return +func (c *DepositCommand) Command() *cobra.Command { + cmd := &cobra.Command{ + Use: "deposit", + Short: `deposit funds into the Vela system`, + Long: `deposit funds into the Vela system`, + Run: func(cmd *cobra.Command, args []string) { + if err := c.Exec(resolveContext(cmd)); err != nil { + fmt.Printf("Error: %v\n", err) } - - fmt.Println("Deposit completed successfully") - }, } cmd.Flags().StringVarP(&c.depositAmount, "amount", "a", "", "The amount to deposit (e.g., '1.5 ETH', '100' for ERC-20)") cmd.Flags().StringVarP(&c.maxFeeValue, "max-value-fee", "f", "100 wei", "Maximum fee value reserved for this request (always ETH)") cmd.Flags().StringVarP(&c.token, "token", "k", "", "Token symbol or address (default: ETH)") - return cmd } diff --git a/wallet/cmd/getprivatebalance.go b/wallet/cmd/getprivatebalance.go index 149c317..ea3cd63 100644 --- a/wallet/cmd/getprivatebalance.go +++ b/wallet/cmd/getprivatebalance.go @@ -5,13 +5,14 @@ import ( "encoding/json" "fmt" "log" + "math/big" "strings" + "github.com/HorizenOfficial/vela-common-go/subgraph" "github.com/HorizenOfficial/vela-nova/wallet/app" "github.com/HorizenOfficial/vela/pkg/blockchain" "github.com/HorizenOfficial/vela/pkg/common" cryptotypes "github.com/HorizenOfficial/vela/pkg/common/crypto" - "github.com/HorizenOfficial/vela-common-go/subgraph" "github.com/spf13/cobra" ) @@ -115,78 +116,84 @@ func findTokenBalance( return "", nil // not found } -func (c *GetPrivateBalanceCommand) Command() *cobra.Command { - cmd := &cobra.Command{ - Use: "getprivatebalance", - Short: `get private balance associated to the wallet address`, - Long: `get private balance associated to the wallet address`, - Run: func(cmd *cobra.Command, args []string) { - if err := c.RequireApplicationID(); err != nil { - log.Fatalf("Error: %v", err) - } +// Exec runs the getprivatebalance flow outside of the Cobra wrapper. Returns +// (balance, tokenInfo, scanDepth, err). A nil balance with nil err means +// "no balance found within scanDepth events" — callers should communicate this +// distinctly from a zero balance. Exported so test drivers can invoke it +// directly after setting flag-bound struct fields. +func (c *GetPrivateBalanceCommand) Exec(ctx context.Context) (*big.Int, *app.TokenInfo, int, error) { + if err := c.RequireApplicationID(); err != nil { + return nil, nil, 0, err + } - // Resolve token - tokenInfo, err := c.Config.Tokens.ResolveToken(c.token) - if err != nil { - log.Fatalf("Error: %v", err) - } + tokenInfo, err := c.Config.Tokens.ResolveToken(c.token) + if err != nil { + return nil, nil, 0, err + } - ctx := context.Background() - if cmd != nil && cmd.Context() != nil { - ctx = cmd.Context() - } - blockchainClient := c.BlockchainClient - if blockchainClient == nil { - if err := c.InitChainClient(ctx); err != nil { - log.Fatalf("Error connecting to rpc node: %v", err) - return - } - blockchainClient = c.BlockchainClient - } - defer blockchainClient.Close() + if err := c.InitChainClient(ctx); err != nil { + return nil, tokenInfo, 0, fmt.Errorf("connecting to rpc node: %w", err) + } + defer c.CloseClient() - if c.SubgraphClient == nil { - log.Fatal("subgraph client not initialized (missing SubgraphURL)") - } + if c.SubgraphClient == nil { + return nil, tokenInfo, 0, fmt.Errorf("subgraph client not initialized (missing SubgraphURL)") + } - teePubKey, err := blockchainClient.GetTeePublicKey(ctx) - if err != nil { - log.Fatalf("failed to get TEE public key: %v", err) - } + teePubKey, err := c.BlockchainClient.GetTeePublicKey(ctx) + if err != nil { + return nil, tokenInfo, 0, fmt.Errorf("failed to get TEE public key: %w", err) + } - if c.Config.KeySecp == nil { - log.Fatal("secp256k1 key not found in the wallet (required to derive event subtypes)") - } - seed, seedErr := GenerateSeed(c.Config.KeySecp) - if seedErr != nil { - log.Fatalf("failed to generate seed: %v", seedErr) - } - seedSubTypes := EventSubTypesFromSeed(seed, DefaultSubtypeN) + if c.Config.KeySecp == nil { + return nil, tokenInfo, 0, fmt.Errorf("secp256k1 key not found in the wallet (required to derive event subtypes)") + } + seed, err := GenerateSeed(c.Config.KeySecp) + if err != nil { + return nil, tokenInfo, 0, fmt.Errorf("failed to generate seed: %w", err) + } + seedSubTypes := EventSubTypesFromSeed(seed, DefaultSubtypeN) - tokenHex := strings.ToLower(tokenInfo.Address.Hex()) + tokenHex := strings.ToLower(tokenInfo.Address.Hex()) - scanDepth := c.Config.PrivateBalanceScanDepth - if scanDepth <= 0 { - scanDepth = defaultScanDepth - } + scanDepth := c.Config.PrivateBalanceScanDepth + if scanDepth <= 0 { + scanDepth = defaultScanDepth + } + + balanceHex, err := findTokenBalance(ctx, c.SubgraphClient, teePubKey, c.Config.KeyP521, c.Config.ApplicationID, seedSubTypes, tokenHex, scanDepth) + if err != nil { + return nil, tokenInfo, scanDepth, fmt.Errorf("failed to find balance: %w", err) + } + + if balanceHex == "" { + return nil, tokenInfo, scanDepth, nil + } + + var balance common.Big + if err := json.Unmarshal([]byte(`"`+balanceHex+`"`), &balance); err != nil { + return nil, tokenInfo, scanDepth, fmt.Errorf("failed to parse balance: %w", err) + } + + return balance.ToInt(), tokenInfo, scanDepth, nil +} - balanceHex, err := findTokenBalance(ctx, c.SubgraphClient, teePubKey, c.Config.KeyP521, c.Config.ApplicationID, seedSubTypes, tokenHex, scanDepth) +func (c *GetPrivateBalanceCommand) Command() *cobra.Command { + cmd := &cobra.Command{ + Use: "getprivatebalance", + Short: `get private balance associated to the wallet address`, + Long: `get private balance associated to the wallet address`, + Run: func(cmd *cobra.Command, args []string) { + balance, tokenInfo, scanDepth, err := c.Exec(resolveContext(cmd)) if err != nil { - log.Fatalf("failed to find balance: %v", err) + log.Fatalf("Error: %v", err) } - - if balanceHex == "" { + if balance == nil { fmt.Printf("No %s balance found in the last %d events.\n", tokenInfo.Symbol, scanDepth) fmt.Println("For an authoritative balance, use: requestreport --report-type balances") return } - - var balance common.Big - if err := json.Unmarshal([]byte(`"`+balanceHex+`"`), &balance); err != nil { - log.Fatalf("failed to parse balance: %v", err) - } - - fmt.Println(c.Config.Tokens.FormatAmount(balance.ToInt(), tokenInfo)) + fmt.Println(c.Config.Tokens.FormatAmount(balance, tokenInfo)) }, } cmd.Flags().StringVarP(&c.token, "token", "k", "", "Token symbol or address (default: ETH)") diff --git a/wallet/cmd/registeruser.go b/wallet/cmd/registeruser.go index e988a1f..c6c77e6 100644 --- a/wallet/cmd/registeruser.go +++ b/wallet/cmd/registeruser.go @@ -22,73 +22,62 @@ type RegisterUserCommand struct { maxFeeValue string } -func (c *RegisterUserCommand) Command() *cobra.Command { - cmd := &cobra.Command{ - Use: "registeruser", - Short: `register the association [address, encryption key (P521)] of the wallet into the Vela system`, - Long: `register the association [address, encryption key (P521)] of the wallet into the Vela system`, - Run: func(cmd *cobra.Command, args []string) { - if err := c.RequireApplicationID(); err != nil { - fmt.Printf("Error: %v\n", err) - return - } +// Exec runs the registeruser flow outside of the Cobra wrapper. Exported so +// test drivers can invoke it directly after setting flag-bound struct fields. +func (c *RegisterUserCommand) Exec(ctx context.Context) error { + if err := c.RequireApplicationID(); err != nil { + return err + } - maxFeeValue, err := app.ParseEtherValue(c.maxFeeValue) - if err != nil { - fmt.Printf("Error: invalid max fee amount: %v\n", err) - return - } + maxFeeValue, err := app.ParseEtherValue(c.maxFeeValue) + if err != nil { + return fmt.Errorf("invalid max fee amount: %w", err) + } - ctx := context.Background() - if cmd != nil && cmd.Context() != nil { - ctx = cmd.Context() - } - if c.BlockchainClient == nil { - //create blockchain client - if err := c.InitChainClient(ctx); err != nil { - fmt.Printf("Error connecting to rpc node: %v\n", err) - return + if err := c.InitChainClient(ctx); err != nil { + return fmt.Errorf("connecting to rpc node: %w", err) + } + defer c.CloseClient() - } - } - defer c.CloseClient() + if c.Config.KeyP521 == nil { + return fmt.Errorf("P521 key not found in the wallet") + } + if c.Config.KeySecp == nil { + return fmt.Errorf("secp256k1 key not found in the wallet") + } - if c.Config.KeyP521 == nil { - fmt.Println("Error: P521 key not found in the wallet") - return - } + teePubKey, err := c.BlockchainClient.GetTeePublicKey(ctx) + if err != nil { + return fmt.Errorf("retrieving TEE public key: %w", err) + } - if c.Config.KeySecp == nil { - fmt.Println("Error: secp256k1 key not found in the wallet") - return - } - teePubKey, err := c.BlockchainClient.GetTeePublicKey(ctx) - if err != nil { - fmt.Printf("Error retrieving TEE public key: %v\n", err) - return - } - payload, err := BuildAssociateKeyPayloadWithSeed(c.Config.KeyP521, c.Config.KeySecp, teePubKey) - if err != nil { - fmt.Printf("Error building payload with seed: %v\n", err) - return - } + payload, err := BuildAssociateKeyPayloadWithSeed(c.Config.KeyP521, c.Config.KeySecp, teePubKey) + if err != nil { + return fmt.Errorf("building payload with seed: %w", err) + } - requestType := common.AssociateKey - requestID, _, err := c.BlockchainClient.SubmitRequest(ctx, PROTOCOL_VERSION, c.Config.ApplicationID, requestType, payload, ETH_TOKEN, big.NewInt(0), maxFeeValue) - if err != nil { - fmt.Printf("Error sending request to register public key: %v\n", err) - return - } + requestID, _, err := c.BlockchainClient.SubmitRequest(ctx, PROTOCOL_VERSION, c.Config.ApplicationID, common.AssociateKey, payload, ETH_TOKEN, big.NewInt(0), maxFeeValue) + if err != nil { + return fmt.Errorf("sending request to register public key: %w", err) + } - fmt.Println("Waiting for confirmation from Vela") + fmt.Println("Waiting for confirmation from Vela") + if err := c.WaitForRequestCompleted(requestID, ctx); err != nil { + return fmt.Errorf("Register user failed: %w", err) + } + fmt.Println("Public key registered successfully") + return nil +} - err = c.WaitForRequestCompleted(requestID, ctx) - if err != nil { - fmt.Printf("Register user failed: %v\n", err) - return +func (c *RegisterUserCommand) Command() *cobra.Command { + cmd := &cobra.Command{ + Use: "registeruser", + Short: `register the association [address, encryption key (P521)] of the wallet into the Vela system`, + Long: `register the association [address, encryption key (P521)] of the wallet into the Vela system`, + Run: func(cmd *cobra.Command, args []string) { + if err := c.Exec(resolveContext(cmd)); err != nil { + fmt.Printf("Error: %v\n", err) } - fmt.Println("Public key registered successfully") - }, } cmd.Flags().StringVarP(&c.maxFeeValue, "max-value-fee", "f", "100 wei", "Maximum fee value reserved for this request (e.g., 0.1 ETH)") diff --git a/wallet/cmd/withdraw.go b/wallet/cmd/withdraw.go index 0b3cb17..0e0be19 100644 --- a/wallet/cmd/withdraw.go +++ b/wallet/cmd/withdraw.go @@ -22,114 +22,91 @@ type WithdrawCommand struct { } func NewWithdrawCommand(config *app.Config, blockchainClient blockchain.Client) *WithdrawCommand { - cmd := &WithdrawCommand{ + return &WithdrawCommand{ ChainCommand: app.NewChainCommand(config, blockchainClient), } - - return cmd } -func (c *WithdrawCommand) Command() *cobra.Command { - cmd := &cobra.Command{ - Use: "withdraw", - Short: `withdraw funds from the Vela system`, - Long: `withdraw funds from the Vela system and send them to a receiver address`, - Run: func(cmd *cobra.Command, args []string) { - if err := c.RequireApplicationID(); err != nil { - fmt.Printf("Error: %v\n", err) - return - } +// Exec runs the withdraw flow outside of the Cobra wrapper. Exported so test +// drivers can invoke it directly after setting flag-bound struct fields. +func (c *WithdrawCommand) Exec(ctx context.Context) error { + if err := c.RequireApplicationID(); err != nil { + return err + } - // Resolve token - tokenInfo, err := c.Config.Tokens.ResolveToken(c.token) - if err != nil { - fmt.Printf("Error: %v\n", err) - return - } + tokenInfo, err := c.Config.Tokens.ResolveToken(c.token) + if err != nil { + return err + } - // Parse amount with correct decimals - var amount *big.Int - if tokenInfo.Address == ETH_TOKEN { - amount, err = app.ParseEtherValue(c.value) - } else { - amount, err = c.Config.Tokens.ParseAmount(c.value, tokenInfo) - } - if err != nil { - fmt.Printf("Error: invalid amount: %v\n", err) - return - } + amount, err := parseAssetAmount(c.Config.Tokens, tokenInfo, c.value) + if err != nil { + return fmt.Errorf("invalid amount: %w", err) + } - maxFeeValue, err := app.ParseEtherValue(c.maxFeeValue) - if err != nil { - fmt.Printf("Error: invalid max fee amount: %v\n", err) - return - } + maxFeeValue, err := app.ParseEtherValue(c.maxFeeValue) + if err != nil { + return fmt.Errorf("invalid max fee amount: %w", err) + } - receiver, err := app.ValidateAndChecksumAddress(c.receiver) - if err != nil { - fmt.Printf("Error: invalid receiver address: %v\n", err) - return - } + receiver, err := app.ValidateAndChecksumAddress(c.receiver) + if err != nil { + return fmt.Errorf("invalid receiver address: %w", err) + } - ctx := context.Background() - if cmd != nil && cmd.Context() != nil { - ctx = cmd.Context() - } - if c.BlockchainClient == nil { - if err := c.InitChainClient(ctx); err != nil { - fmt.Printf("Error connecting to rpc node: %v\n", err) - return - } - } - defer c.CloseClient() + if err := c.InitChainClient(ctx); err != nil { + return fmt.Errorf("connecting to rpc node: %w", err) + } + defer c.CloseClient() - receiverAddr, err := types.HexToAddress(receiver.Hex()) - if err != nil { - fmt.Printf("Error: invalid receiver address, could not convert to byte array: %v\n", err) - return - } + receiverAddr, err := types.HexToAddress(receiver.Hex()) + if err != nil { + return fmt.Errorf("invalid receiver address, could not convert to byte array: %w", err) + } + tokenAddr, err := types.HexToAddress(tokenInfo.Address.Hex()) + if err != nil { + return fmt.Errorf("invalid token address: %w", err) + } - // Resolve token address for the encrypted payload - tokenAddr, err := types.HexToAddress(tokenInfo.Address.Hex()) - if err != nil { - fmt.Printf("Error: invalid token address: %v\n", err) - return - } + payload := runtimeapp.PayloadInstructions{ + Type: "withdraw", + Withdraw: &runtimeapp.WithdrawInstruction{ + To: receiverAddr, + TokenAddress: tokenAddr, + Amount: new(types.Uint256).SetBytes(amount.Bytes()), + }, + } + encryptedPayload, err := c.EncryptPayload(&payload, ctx) + if err != nil { + return fmt.Errorf("encrypting withdraw payload: %w", err) + } - payload := runtimeapp.PayloadInstructions{ - Type: "withdraw", - Withdraw: &runtimeapp.WithdrawInstruction{ - To: receiverAddr, - TokenAddress: tokenAddr, - Amount: new(types.Uint256).SetBytes(amount.Bytes()), - }, - } - encryptedPayload, err := c.EncryptPayload(&payload, ctx) - if err != nil { - fmt.Printf("Error encrypting withdraw payload: %v\n", err) - return - } + // On-chain: no business asset deposited (withdrawal is from private state), + // so tokenAddress=ETH_TOKEN and assetAmount=0. + requestID, _, err := c.BlockchainClient.SubmitRequest(ctx, PROTOCOL_VERSION, c.Config.ApplicationID, common.Process, encryptedPayload, ETH_TOKEN, big.NewInt(0), maxFeeValue) + if err != nil { + return fmt.Errorf("sending request to withdraw %s %s: %w", c.value, tokenInfo.Symbol, err) + } - // On-chain: no business asset deposited (withdrawal is from private state), - // so tokenAddress=ETH_TOKEN and assetAmount=0. - requestType := common.Process - requestID, _, err := c.BlockchainClient.SubmitRequest(ctx, PROTOCOL_VERSION, c.Config.ApplicationID, requestType, encryptedPayload, ETH_TOKEN, big.NewInt(0), maxFeeValue) - if err != nil { - fmt.Printf("Error sending request to withdraw %s %s: %v\n", c.value, tokenInfo.Symbol, err) - return - } + fmt.Println("Waiting for confirmation from Vela") + if err := c.WaitForRequestCompleted(requestID, ctx); err != nil { + return fmt.Errorf("Withdrawal failed: %w", err) + } - fmt.Println("Waiting for confirmation from Vela") + fmt.Println("Withdrawal completed successfully, funds moved from the private state to the bridge contract and ready to be claimed") + fmt.Printf("IMPORTANT: execute 'claimpendingpayments --token %s' to receive funds in your public address\n", tokenInfo.Symbol) + return nil +} - err = c.WaitForRequestCompleted(requestID, ctx) - if err != nil { - fmt.Printf("Withdrawal failed: %v\n", err) - return +func (c *WithdrawCommand) Command() *cobra.Command { + cmd := &cobra.Command{ + Use: "withdraw", + Short: `withdraw funds from the Vela system`, + Long: `withdraw funds from the Vela system and send them to a receiver address`, + Run: func(cmd *cobra.Command, args []string) { + if err := c.Exec(resolveContext(cmd)); err != nil { + fmt.Printf("Error: %v\n", err) } - - fmt.Println("Withdrawal completed successfully, funds moved from the private state to the bridge contract and ready to be claimed") - fmt.Printf("IMPORTANT: execute 'claimpendingpayments --token %s' to receive funds in your public address\n", tokenInfo.Symbol) - }, } cmd.Flags().StringVarP(&c.value, "amount", "a", "", "The amount to withdraw (e.g., '1.5 ETH', '100' for ERC-20)") diff --git a/wallet/go.mod b/wallet/go.mod index d968cb2..c2b1961 100644 --- a/wallet/go.mod +++ b/wallet/go.mod @@ -5,7 +5,7 @@ go 1.24.0 toolchain go1.24.3 // Can be useful for local developments -//replace github.com/HorizenOfficial/vela => ../../vela +replace github.com/HorizenOfficial/vela => ../../vela //replace github.com/HorizenOfficial/vela-common-go => ../../vela-common-go @@ -27,8 +27,23 @@ require ( github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect github.com/StackExchange/wmi v1.2.1 // indirect github.com/VictoriaMetrics/fastcache v1.13.0 // indirect + github.com/aws/aws-sdk-go-v2 v1.41.1 // indirect + github.com/aws/aws-sdk-go-v2/config v1.29.14 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect + github.com/aws/aws-sdk-go-v2/service/kms v1.49.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect + github.com/aws/smithy-go v1.24.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.20.0 // indirect + github.com/bytecodealliance/wasmtime-go v1.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cockroachdb/errors v1.11.3 // indirect github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce // indirect @@ -49,7 +64,9 @@ require ( github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab // indirect github.com/ferranbt/fastssz v0.1.4 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/fxamacker/cbor/v2 v2.2.0 // indirect github.com/getsentry/sentry-go v0.27.0 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect @@ -63,6 +80,7 @@ require ( github.com/grafana/pyroscope-go v1.2.7 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect github.com/hashicorp/go-bexpr v0.1.10 // indirect + github.com/hf/nsm v0.0.0-20220930140112-cd181bd646b9 // indirect github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect github.com/holiman/uint256 v1.3.2 // indirect @@ -76,6 +94,8 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/mdlayher/socket v0.4.1 // indirect + github.com/mdlayher/vsock v1.2.1 // indirect github.com/minio/sha256-simd v1.0.0 // indirect github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/mitchellh/pointerstructure v1.2.0 // indirect @@ -92,6 +112,7 @@ require ( github.com/prometheus/procfs v0.9.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rs/cors v1.7.0 // indirect + github.com/rs/zerolog v1.34.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/spf13/pflag v1.0.6 // indirect @@ -100,6 +121,7 @@ require ( github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/urfave/cli/v2 v2.27.5 // indirect + github.com/x448/float16 v0.8.4 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.40.0 // indirect @@ -107,6 +129,7 @@ require ( go.opentelemetry.io/otel/trace v1.40.0 // indirect golang.org/x/crypto v0.44.0 // indirect golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect + golang.org/x/net v0.47.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/text v0.31.0 // indirect diff --git a/wallet/go.sum b/wallet/go.sum index 565a1d4..fd16f01 100644 --- a/wallet/go.sum +++ b/wallet/go.sum @@ -1,7 +1,5 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= -github.com/HorizenOfficial/vela v0.0.28 h1:xmXA9PMUV64msBb84mi0VwXQuqMJXPX2wVPBCgue8c8= -github.com/HorizenOfficial/vela v0.0.28/go.mod h1:E/SXS/mBVYakoFwzEmMMw8w5BUxIv+geUhwB5J0JRcE= github.com/HorizenOfficial/vela-common-go v0.0.28 h1:iVeJMUYIDJ83eAzoS0+gMQRjnQj9/yKv9cp2ev3PFNg= github.com/HorizenOfficial/vela-common-go v0.0.28/go.mod h1:VGEDebTDUnwrnMycUHQtm9WBi0DpRcYjsetIU4KbRmc= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -14,10 +12,40 @@ github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMG github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= +github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= +github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM= +github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g= +github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM= +github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= +github.com/aws/aws-sdk-go-v2/service/kms v1.49.5 h1:DKibav4XF66XSeaXcrn9GlWGHos6D/vJ4r7jsK7z5CE= +github.com/aws/aws-sdk-go-v2/service/kms v1.49.5/go.mod h1:1SdcmEGUEQE1mrU2sIgeHtcMSxHuybhPvuEPANzIDfI= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= +github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= +github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bytecodealliance/wasmtime-go v1.0.0 h1:9u9gqaUiaJeN5IoD1L7egD8atOnTGyJcNp8BhkL9cUU= +github.com/bytecodealliance/wasmtime-go v1.0.0/go.mod h1:jjlqQbWUfVSbehpErw3UoWFndBXRRMvfikYH6KsCwOg= github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -38,6 +66,7 @@ github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAK github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= github.com/consensys/gnark-crypto v0.18.1 h1:RyLV6UhPRoYYzaFnPQA4qK3DyuDgkTgskDdoGqFt3fI= github.com/consensys/gnark-crypto v0.18.1/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc= @@ -72,10 +101,14 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fxamacker/cbor/v2 v2.2.0 h1:6eXqdDDe588rSYAi1HfZKbx6YYQO4mxQ9eC6xYpU/JQ= +github.com/fxamacker/cbor/v2 v2.2.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= +github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -86,6 +119,7 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -124,6 +158,8 @@ github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY4 github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/hf/nsm v0.0.0-20220930140112-cd181bd646b9 h1:pU32bJGmZwF4WXb9Yaz0T8vHDtIPVxqDOdmYdwTQPqw= +github.com/hf/nsm v0.0.0-20220930140112-cd181bd646b9/go.mod h1:MJsac5D0fKcNWfriUERtln6segcGfD6Nu0V5uGBbPf8= github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg= github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= @@ -163,12 +199,17 @@ github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= +github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= +github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ= +github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE= github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= @@ -222,6 +263,9 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= @@ -252,6 +296,8 @@ github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+F github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -279,6 +325,8 @@ golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -331,6 +379,7 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -352,7 +401,9 @@ golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210105210202-9ed45478a130/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= diff --git a/wallet/testutil/wallet_driver.go b/wallet/testutil/wallet_driver.go new file mode 100644 index 0000000..acf7792 --- /dev/null +++ b/wallet/testutil/wallet_driver.go @@ -0,0 +1,180 @@ +// Package testutil provides WalletDriver, an in-process harness that drives +// vela-nova wallet commands against a FullStackSystemTestSuite. +// +// Configuration follows a "hybrid" pattern: the driver writes a real temp +// wallet.conf via app.SaveConfigToFile, then calls app.LoadConfigFromFile so +// the production config-loading path is exercised on every run. The only +// deliberate departure is subgraph injection: the loaded config's SubgraphURL +// is empty, so the driver assigns suite.GetSubgraph() directly onto each +// constructed ChainCommand (the SubgraphClient field is public). +// +// Unlike the suite's own wrapped blockchain client (bound to the manager's +// key), the driver builds its own blockchain.Client bound to the user's +// secp256k1 key, so transactions submitted by the wallet are signed by the +// user. During construction it also grants the user the ProcessorEndpoint +// DEPLOYER_ROLE so wallet-driven DeployApp works end-to-end. +// +// Typed wrapper methods (DeployApp, RegisterUser, Deposit, ...) set +// flag-bound struct fields via cobra.Command.Flags().Set() — this exercises +// Cobra's flag parsing — then call each command's exported Exec(ctx) method +// for the business logic. Errors propagate to the caller so tests can assert. +package testutil + +import ( + "path/filepath" + "testing" + + "github.com/HorizenOfficial/vela-nova/wallet/app" + "github.com/HorizenOfficial/vela/pkg/blockchain" + "github.com/HorizenOfficial/vela/pkg/common" + cryptotypes "github.com/HorizenOfficial/vela/pkg/common/crypto" + "github.com/HorizenOfficial/vela/pkg/crypto" + "github.com/HorizenOfficial/vela/pkg/testutil/fullstack" + ethCommon "github.com/ethereum/go-ethereum/common" + ethCrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" +) + +// unreachableRpcURL is a placeholder required by LoadConfigFromFile's URL +// parser. The driver never dials it — the blockchain.Client is injected +// pre-connected from the suite. +const unreachableRpcURL = "http://127.0.0.1:0" + +// WalletDriver drives vela-nova wallet commands end-to-end against a +// FullStackSystemTestSuite. It owns a temp wallet.conf and a user identity +// (secp256k1 for on-chain, P521 for private-state encryption). A fresh +// user-signed blockchain.Client is built per wrapper invocation (commands' +// defer-Close() would otherwise kill a shared client after the first call). +type WalletDriver struct { + t *testing.T + suite *fullstack.FullStackSystemTestSuite + confPath string + userAddr ethCommon.Address // cached for per-call client construction +} + +// NewWalletDriver constructs a WalletDriver for a fresh user. It generates a +// funded secp256k1 key via the suite, generates a P521 key, writes a temp +// wallet.conf populated from the suite's addresses, and re-loads it through +// the production LoadConfigFromFile path. Also grants the user the +// ProcessorEndpoint DEPLOYER_ROLE. +func NewWalletDriver(t *testing.T, suite *fullstack.FullStackSystemTestSuite) *WalletDriver { + t.Helper() + + _, secpKey, err := suite.CreateFundedAccount() + require.NoError(t, err) + + p521Key, err := crypto.GeneratePrivateKeyP521() + require.NoError(t, err) + + return NewWalletDriverWithKeys(t, suite, secpKey, p521Key) +} + +// NewWalletDriverWithKeys is like NewWalletDriver but uses caller-supplied +// keys. The user's secp key is registered with the suite (funded with ETH, +// TransactOpts stored) and granted DEPLOYER_ROLE on the ProcessorEndpoint. +func NewWalletDriverWithKeys( + t *testing.T, + suite *fullstack.FullStackSystemTestSuite, + secpKey *cryptotypes.PrivateKeySecp256k1, + p521Key *cryptotypes.PrivateKeyP521, +) *WalletDriver { + t.Helper() + + // Ensure the user account is funded and has a TransactOpts registered with + // the suite. Skip if already registered (e.g. via NewWalletDriver → + // CreateFundedAccount) — a second RegisterAccount would re-fund 5 ETH and + // can overrun the deployer's balance. + userAddr := ethCrypto.PubkeyToAddress(secpKey.PrivateKey.PublicKey) + if !suite.HasAccount(userAddr) { + suite.RegisterAccount(secpKey.PrivateKey) + } + + // Grant DEPLOYER_ROLE so wallet-driven DeployApp can submit deploy requests. + suite.GrantDeployerRole(userAddr) + + sim := suite.GetSimTestHelper() + processor := sim.ProcessorContractAddress + teeAuth := sim.TeeSignerAddress + + // Seed the registry with just the implicit ETH entry. Phase 5 will add + // ERC-20 tokens by writing token.* conf entries before SaveConfigToFile. + tokens, err := app.LoadTokenRegistry(nil) + require.NoError(t, err) + + cfg := &app.Config{ + KeySecp: secpKey, + KeyP521: p521Key, + RpcUrl: unreachableRpcURL, + ProcessorEndpointAddress: &processor, + TeeAuthenticatorAddress: &teeAuth, + AuthorityServiceURL: suite.GetAuthorityServiceURL(), + SubgraphURL: "", // injected in-process; no HTTP shim + ApplicationID: 0, // set by DeployApp + BlockchainPollingInterval: 1, + BlockchainPollingTimeout: 60, + PrivateBalanceScanDepth: 200, + Tokens: tokens, + } + + confPath := filepath.Join(t.TempDir(), app.ConfFileName) + require.NoError(t, app.SaveConfigToFile(cfg, confPath)) + + // Exercise the production load path to catch conf/file drift. + if _, err := app.LoadConfigFromFile(confPath); err != nil { + t.Fatalf("wallet driver: initial LoadConfigFromFile failed: %v", err) + } + + return &WalletDriver{ + t: t, + suite: suite, + confPath: confPath, + userAddr: userAddr, + } +} + +// newUserClient builds a fresh user-signed blockchain.Client. Commands +// defer-Close() their BlockchainClient, so we can't share one across calls — +// each wrapper gets its own pre-connected client bound to the user's key. +func (d *WalletDriver) newUserClient() blockchain.Client { + d.t.Helper() + sim := d.suite.GetSimTestHelper() + userOpts, err := d.suite.GetTransactOpts(d.userAddr) + require.NoError(d.t, err) + return blockchain.SetupNewBlockChainClientConnected( + sim.Client(), sim.ProcessorContractAddress, sim.TeeSignerAddress, userOpts, + ) +} + +// loadConfig re-reads the temp conf file each call. Fresh loads pick up +// ApplicationID persisted by DeployApp's SaveApplicationID, mirroring how a +// real CLI session picks up state between invocations. +func (d *WalletDriver) loadConfig() *app.Config { + d.t.Helper() + cfg, err := app.LoadConfigFromFile(d.confPath) + require.NoError(d.t, err) + return cfg +} + +// UserAddress returns the secp256k1-derived address of the wallet's user. +func (d *WalletDriver) UserAddress() ethCommon.Address { + return ethCommon.HexToAddress(d.loadConfig().KeySecp.PublicKey().Address()) +} + +// ApplicationID returns the currently configured application ID (0 before DeployApp). +func (d *WalletDriver) ApplicationID() common.ApplicationIdType { + return d.loadConfig().ApplicationID +} + +// ConfPath returns the path of the temp wallet.conf. Useful for tests that +// need to inspect the file between commands. +func (d *WalletDriver) ConfPath() string { + return d.confPath +} + +// NewBlockchainClient returns a fresh user-bound blockchain.Client. Exposed +// so tests can make direct on-chain queries (e.g. public balance) against +// the same chain view the wallet uses. Each call returns a new pre-connected +// client — the caller is responsible for Close() when done. +func (d *WalletDriver) NewBlockchainClient() blockchain.Client { + return d.newUserClient() +} diff --git a/wallet/testutil/wallet_driver_test.go b/wallet/testutil/wallet_driver_test.go new file mode 100644 index 0000000..8611854 --- /dev/null +++ b/wallet/testutil/wallet_driver_test.go @@ -0,0 +1,75 @@ +package testutil_test + +import ( + "math/big" + "os" + "path/filepath" + "testing" + + "github.com/HorizenOfficial/vela-nova/wallet/testutil" + "github.com/HorizenOfficial/vela/pkg/executor" + "github.com/HorizenOfficial/vela/pkg/logger" + "github.com/HorizenOfficial/vela/pkg/manager" + "github.com/HorizenOfficial/vela/pkg/testutil/fullstack" + "github.com/stretchr/testify/require" +) + +// TestWalletDriver_DeployRegisterDeposit is the Phase 4 end-to-end smoke. +// It drives three wallet commands (DeployApp, RegisterUser, Deposit) against +// a real simulated chain + in-process subgraph + in-process authority + +// mock-runtime executor. Assertions target on-chain custody and subgraph +// state rather than stdout — the driver returns structured errors, and the +// subgraph is the same source the wallet's WaitForRequestCompleted polls. +// +// The runtime is mock-runtime so no TinyGo build is required; any bytes are +// accepted as the "WASM" artifact. This keeps the test hermetic. +func TestWalletDriver_DeployRegisterDeposit(t *testing.T) { + if os.Getenv("CI_FLAG") != "" { + t.Skip("skipping fullstack test under CI_FLAG") + } + + suite := newSuite(t) + defer func() { _ = suite.Cleanup() }() + + require.NoError(t, suite.StartExecutor()) + require.NoError(t, suite.StartManager()) + + driver := testutil.NewWalletDriver(t, suite) + + // Placeholder WASM — mock-runtime ignores contents. + wasmPath := filepath.Join(t.TempDir(), "app.wasm") + require.NoError(t, os.WriteFile(wasmPath, []byte("mock-wasm-bytes"), 0o644)) + + appID, err := driver.DeployApp(t.Context(), wasmPath, "100 wei", nil) + require.NoError(t, err) + require.NotZero(t, appID, "DeployApp must assign a non-zero ApplicationID") + require.Equal(t, appID, driver.ApplicationID(), "driver should reflect persisted ApplicationID") + + require.NoError(t, driver.RegisterUser(t.Context(), "100 wei")) + + // Deposit 0.1 ETH (1e17 wei). + require.NoError(t, driver.Deposit(t.Context(), "0.1 ETH", "", "100 wei")) + + // On-chain custody for ETH (address 0) must reflect the deposit. + ethAddr := suite.GetSimTestHelper().Submitter.From + _ = ethAddr // avoid unused; keep in case debugging needs sender context + ethZero := [20]byte{} + custody := suite.GetAppCustody(appID, ethZero) + expected := new(big.Int).Exp(big.NewInt(10), big.NewInt(17), nil) // 1e17 + require.Equal(t, 0, custody.Cmp(expected), + "on-chain appCustody for ETH should equal 0.1 ETH; got %s wei", custody.String()) +} + +func newSuite(t *testing.T) *fullstack.FullStackSystemTestSuite { + t.Helper() + t.Setenv("MANAGER_ARTIFACTS_PATH", t.TempDir()) + t.Setenv("EXECUTOR_KEYSET_RECOVERY_TYPE", "0") + mgrCfg, err := manager.LoadConfig() + require.NoError(t, err) + execCfg, err := executor.LoadConfig() + require.NoError(t, err) + keySet, recovery, err := executor.GenerateEnclaveKeySet(t.Context(), execCfg.KeySetRecoveryType, nil, nil, "") + require.NoError(t, err) + logCfg := &logger.Config{Kind: "zerolog", Console: true, ConsoleLevel: "info"} + return fullstack.NewFullStackSystemTestSuiteWithConfigs(t, "mock-runtime", mgrCfg, execCfg, keySet, recovery, logCfg, logCfg) +} diff --git a/wallet/testutil/wrappers.go b/wallet/testutil/wrappers.go new file mode 100644 index 0000000..cd9f274 --- /dev/null +++ b/wallet/testutil/wrappers.go @@ -0,0 +1,141 @@ +package testutil + +import ( + "context" + "fmt" + "math/big" + + "github.com/HorizenOfficial/vela-nova/wallet/cmd" + "github.com/HorizenOfficial/vela/pkg/common" + "github.com/stretchr/testify/require" +) + +// DeployApp uploads a WASM artifact via the authority service, submits a +// deploy request on-chain, and waits for completion. Persists the assigned +// ApplicationID into the temp wallet.conf so subsequent commands pick it up. +// Returns the assigned ApplicationID. +// +// allowedTokens may be nil (ETH is always allowed). ERC-20 symbols require +// those tokens to be registered in the driver's conf; that lands with Phase 5. +func (d *WalletDriver) DeployApp(ctx context.Context, wasmPath, maxFee string, allowedTokens []string) (common.ApplicationIdType, error) { + d.t.Helper() + + cfg := d.loadConfig() + c := cmd.NewDeployAppCommand(cfg, d.newUserClient(), d.confPath) + cobraCmd := c.Command() + require.NoError(d.t, cobraCmd.Flags().Set("wasm", wasmPath)) + require.NoError(d.t, cobraCmd.Flags().Set("max-value-fee", maxFee)) + for _, tok := range allowedTokens { + require.NoError(d.t, cobraCmd.Flags().Set("allowed-tokens", tok)) + } + c.SubgraphClient = d.suite.GetSubgraph() + + if err := c.Exec(ctx); err != nil { + return 0, fmt.Errorf("wallet driver: DeployApp: %w", err) + } + + // Re-read the conf; the command wrote ApplicationID back to disk. + return d.loadConfig().ApplicationID, nil +} + +// RegisterUser submits an AssociateKey request linking the wallet's secp +// address to its P521 public key, so the enclave can encrypt events for it. +func (d *WalletDriver) RegisterUser(ctx context.Context, maxFee string) error { + d.t.Helper() + + cfg := d.loadConfig() + c := cmd.NewRegisterUserCommand(cfg, d.newUserClient()) + cobraCmd := c.Command() + require.NoError(d.t, cobraCmd.Flags().Set("max-value-fee", maxFee)) + c.SubgraphClient = d.suite.GetSubgraph() + + if err := c.Exec(ctx); err != nil { + return fmt.Errorf("wallet driver: RegisterUser: %w", err) + } + return nil +} + +// Deposit submits a deposit request for the given token and amount. +// For ETH, amount is an ether string (e.g. "1.5 ETH", "100 wei"); for ERC-20, +// it's a decimal count in the token's configured decimals. +func (d *WalletDriver) Deposit(ctx context.Context, amount, token, maxFee string) error { + d.t.Helper() + + cfg := d.loadConfig() + c := cmd.NewDepositCommand(cfg, d.newUserClient()) + cobraCmd := c.Command() + require.NoError(d.t, cobraCmd.Flags().Set("amount", amount)) + require.NoError(d.t, cobraCmd.Flags().Set("max-value-fee", maxFee)) + if token != "" { + require.NoError(d.t, cobraCmd.Flags().Set("token", token)) + } + c.SubgraphClient = d.suite.GetSubgraph() + + if err := c.Exec(ctx); err != nil { + return fmt.Errorf("wallet driver: Deposit: %w", err) + } + return nil +} + +// Withdraw submits a withdraw request moving `amount` of `token` from the +// wallet's private balance to `to`. Funds land in pendingClaims on-chain and +// must be claimed via ClaimPendingPayments to reach the user's public balance. +func (d *WalletDriver) Withdraw(ctx context.Context, amount, to, token, maxFee string) error { + d.t.Helper() + + cfg := d.loadConfig() + c := cmd.NewWithdrawCommand(cfg, d.newUserClient()) + cobraCmd := c.Command() + require.NoError(d.t, cobraCmd.Flags().Set("amount", amount)) + require.NoError(d.t, cobraCmd.Flags().Set("to", to)) + require.NoError(d.t, cobraCmd.Flags().Set("max-value-fee", maxFee)) + if token != "" { + require.NoError(d.t, cobraCmd.Flags().Set("token", token)) + } + c.SubgraphClient = d.suite.GetSubgraph() + + if err := c.Exec(ctx); err != nil { + return fmt.Errorf("wallet driver: Withdraw: %w", err) + } + return nil +} + +// GetPrivateBalance returns the wallet's encrypted private balance for the +// given token, decrypted via the wallet's P521 key. Returns nil if no balance +// was found within the configured scan depth (distinct from a zero balance). +func (d *WalletDriver) GetPrivateBalance(ctx context.Context, token string) (*big.Int, error) { + d.t.Helper() + + cfg := d.loadConfig() + c := cmd.NewGetPrivateBalanceCommand(cfg, d.newUserClient()) + cobraCmd := c.Command() + if token != "" { + require.NoError(d.t, cobraCmd.Flags().Set("token", token)) + } + c.SubgraphClient = d.suite.GetSubgraph() + + balance, _, _, err := c.Exec(ctx) + if err != nil { + return nil, fmt.Errorf("wallet driver: GetPrivateBalance: %w", err) + } + return balance, nil +} + +// ClaimPendingPayments pulls pending claims for `token` from the +// ProcessorEndpoint contract into the user's public balance. +func (d *WalletDriver) ClaimPendingPayments(ctx context.Context, token string) error { + d.t.Helper() + + cfg := d.loadConfig() + c := cmd.NewClaimPendingPaymentsCommand(cfg, d.newUserClient()) + cobraCmd := c.Command() + if token != "" { + require.NoError(d.t, cobraCmd.Flags().Set("token", token)) + } + c.SubgraphClient = d.suite.GetSubgraph() + + if err := c.Exec(ctx); err != nil { + return fmt.Errorf("wallet driver: ClaimPendingPayments: %w", err) + } + return nil +} From a4c8aaaf0ba999f45fe77059f3b07bacc9de8040 Mon Sep 17 00:00:00 2001 From: Alberto Sala Date: Wed, 22 Apr 2026 15:36:30 +0200 Subject: [PATCH 07/25] Extract shared WASM test helpers into runtime/wasm-go/testhelpers and add WalletDriver.AddToken for ERC-20 token registration in tests --- .../wasm-go/fullstack_tests/helpers_test.go | 86 --------- .../payment_app_fullstack_test.go | 37 +--- .../system_tests/payment_app_system_test.go | 148 +++------------- runtime/wasm-go/testhelpers/helpers.go | 163 ++++++++++++++++++ wallet/testutil/wallet_driver.go | 34 ++++ 5 files changed, 225 insertions(+), 243 deletions(-) delete mode 100644 runtime/wasm-go/fullstack_tests/helpers_test.go create mode 100644 runtime/wasm-go/testhelpers/helpers.go diff --git a/runtime/wasm-go/fullstack_tests/helpers_test.go b/runtime/wasm-go/fullstack_tests/helpers_test.go deleted file mode 100644 index adba980..0000000 --- a/runtime/wasm-go/fullstack_tests/helpers_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package main_test - -import ( - "bytes" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "mime/multipart" - "net/http" - "net/http/httptest" - "os" - "os/exec" - "path/filepath" - "runtime" - "testing" - - "github.com/HorizenOfficial/vela/pkg/authorityservice/deployartifact" - "github.com/HorizenOfficial/vela/pkg/common" - "github.com/HorizenOfficial/vela/pkg/logger" - "github.com/HorizenOfficial/vela/pkg/testutil/fullstack" - "github.com/stretchr/testify/require" -) - -// buildAndLoadWasmModule builds the payment app WASM module and returns its bytecode. -func buildAndLoadWasmModule(t *testing.T) []byte { - t.Helper() - _, b, _, ok := runtime.Caller(0) - require.True(t, ok) - projectRoot := filepath.Join(filepath.Dir(b), "../..") - appDir := filepath.Join(projectRoot, "wasm-go") - - cmd := exec.Command("make", "build") - cmd.Dir = appDir - output, err := cmd.CombinedOutput() - require.NoError(t, err, "failed to build wasm module: %s", string(output)) - - wasmPath := filepath.Join(appDir, "build", "payment_app.wasm") - wasmBytecode, err := os.ReadFile(wasmPath) - require.NoError(t, err) - require.NotEmpty(t, wasmBytecode) - - return wasmBytecode -} - -// uploadArtifactAndBuildDescriptorPayload uploads a WASM artifact to the local -// artifact store and returns a deploy descriptor payload (JSON) that references it. -func uploadArtifactAndBuildDescriptorPayload(t *testing.T, suite *fullstack.FullStackSystemTestSuite, wasmBytecode []byte) []byte { - t.Helper() - - store, err := deployartifact.NewStore(suite.GetArtifactsPath()) - require.NoError(t, err) - uploadAPI := deployartifact.NewAPI(store, 50, logger.NewLogger(&logger.Config{Kind: "zerolog", Console: false})) - - var body bytes.Buffer - writer := multipart.NewWriter(&body) - fileWriter, err := writer.CreateFormFile("wasm", "app.wasm") - require.NoError(t, err) - _, err = fileWriter.Write(wasmBytecode) - require.NoError(t, err) - require.NoError(t, writer.Close()) - - req := httptest.NewRequest(http.MethodPost, "/deploy/upload", &body) - req.Header.Set("Content-Type", writer.FormDataContentType()) - rr := httptest.NewRecorder() - uploadAPI.HandleUpload(rr, req) - require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) - - var uploadResp deployartifact.UploadResponse - require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &uploadResp)) - - localSum := sha256.Sum256(wasmBytecode) - localSHA := hex.EncodeToString(localSum[:]) - localArtifactID, err := common.BuildArtifactID(localSHA) - require.NoError(t, err) - require.Equal(t, localSHA, uploadResp.WasmSHA256) - require.Equal(t, localArtifactID, uploadResp.ArtifactID) - - descriptor := common.DeployDescriptor{ - Mode: common.DeployModeArtifactRef, - ArtifactID: uploadResp.ArtifactID, - WasmSHA256: uploadResp.WasmSHA256, - } - payload, err := json.Marshal(descriptor) - require.NoError(t, err) - return payload -} diff --git a/runtime/wasm-go/fullstack_tests/payment_app_fullstack_test.go b/runtime/wasm-go/fullstack_tests/payment_app_fullstack_test.go index d4cf783..bf549ba 100644 --- a/runtime/wasm-go/fullstack_tests/payment_app_fullstack_test.go +++ b/runtime/wasm-go/fullstack_tests/payment_app_fullstack_test.go @@ -8,11 +8,10 @@ import ( "time" velacommon "github.com/HorizenOfficial/vela-common-go/common" + "github.com/HorizenOfficial/vela-nova/payment-app/testhelpers" "github.com/HorizenOfficial/vela/pkg/common" - cryptotypes "github.com/HorizenOfficial/vela/pkg/common/crypto" commontestutil "github.com/HorizenOfficial/vela/pkg/common/testutil" "github.com/HorizenOfficial/vela/pkg/executor" - "github.com/HorizenOfficial/vela/pkg/logger" systemTests "github.com/HorizenOfficial/vela/pkg/testutil" "github.com/HorizenOfficial/vela/pkg/testutil/fullstack" ethCommon "github.com/ethereum/go-ethereum/common" @@ -32,19 +31,6 @@ func createFullstackUser(t *testing.T, suite *fullstack.FullStackSystemTestSuite return addr, nil } -// registerSeedUser generates a P521 key, submits an AssociateKey request with -// an encrypted seed, and waits for completion. -func registerSeedUser(t *testing.T, suite *fullstack.FullStackSystemTestSuite, cryptoHelper *systemTests.CryptoHelper, executorPubKey *cryptotypes.PublicKeyP521, appID common.ApplicationIdType, user ethCommon.Address, timeout time.Duration) { - t.Helper() - userKey, err := cryptoHelper.GenerateUserKey(user) - require.NoError(t, err) - reqID := commontestutil.GenerateRandomRequestID() - req, err := cryptoHelper.CreateAssociateKeyRequest(appID, reqID, user, userKey.PublicKey(), executorPubKey) - require.NoError(t, err) - require.NoError(t, suite.SubmitRequest(req)) - require.NoError(t, suite.AssertRequestCompleted(req.RequestID, timeout)) -} - // TestFullStackDeployAndDeposit is the fullstack smoke test: // deploy an app on the real simulated chain, register a user, deposit ETH, // verify the event is received, and query the in-process subgraph for @@ -59,10 +45,10 @@ func TestFullStackDeployAndDeposit(t *testing.T) { t.Setenv("MANAGER_ARTIFACTS_PATH", t.TempDir()) - suite := fullstack.NewFullStackSystemTestSuite(t, "wasmtime-payment", newNetworkLogConfig(), newNetworkLogConfig()) + suite := fullstack.NewFullStackSystemTestSuite(t, "wasmtime-payment", testhelpers.NewNetworkLogConfig(), testhelpers.NewNetworkLogConfig()) defer suite.Cleanup() - wasmBytecode := buildAndLoadWasmModule(t) + wasmBytecode := testhelpers.BuildAndLoadWasmModule(t) require.NoError(t, suite.StartExecutor()) require.NoError(t, suite.StartManager()) @@ -78,7 +64,7 @@ func TestFullStackDeployAndDeposit(t *testing.T) { // Deploy the app — appID is assigned by the contract. // The ProcessorEndpoint contract requires the sender to have the deployer role, // so we use the pre-authorized deployer account from the simulated chain. - deployDescriptor := uploadArtifactAndBuildDescriptorPayload(t, suite, wasmBytecode) + deployDescriptor := testhelpers.UploadArtifactAndBuildDescriptorPayload(t, suite, wasmBytecode) deployReq := &common.Request{ RequestType: common.Deploy, Payload: deployDescriptor, @@ -101,7 +87,7 @@ func TestFullStackDeployAndDeposit(t *testing.T) { require.NoError(t, err) // Register user key (seed-registered) - registerSeedUser(t, suite, cryptoHelper, executorPubKey, appID, userAddress, timeout) + testhelpers.RegisterSeedUser(t, suite, cryptoHelper, executorPubKey, appID, userAddress, timeout) // Deposit 2 ETH depositAmount := big.NewInt(2_000_000_000_000_000_000) @@ -181,16 +167,3 @@ func TestFullStackDeployAndDeposit(t *testing.T) { t.Log("Fullstack deploy + deposit + subgraph verification passed") } -// --- Shared helpers (same as system_tests, adapted for fullstack suite) --- - -func newNetworkLogConfig() *logger.Config { - return &logger.Config{ - Kind: "zeronetwork", - ConsoleColor: false, - Console: true, - ConsoleLevel: "trace", - FileLevel: "trace", - RemoteLogNetwork: "tcp", - NetworkLevel: "trace", - } -} diff --git a/runtime/wasm-go/system_tests/payment_app_system_test.go b/runtime/wasm-go/system_tests/payment_app_system_test.go index 69d3476..da9086c 100644 --- a/runtime/wasm-go/system_tests/payment_app_system_test.go +++ b/runtime/wasm-go/system_tests/payment_app_system_test.go @@ -1,26 +1,20 @@ package main_test import ( - "bytes" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "math/big" - "mime/multipart" - "net/http" - "net/http/httptest" "os" - "os/exec" "path/filepath" - "runtime" "strings" "testing" "time" "github.com/HorizenOfficial/vela-common-go/wasm/types" "github.com/HorizenOfficial/vela-nova/payment-app/app" - "github.com/HorizenOfficial/vela/pkg/authorityservice/deployartifact" + "github.com/HorizenOfficial/vela-nova/payment-app/testhelpers" "github.com/HorizenOfficial/vela/pkg/common" cryptotypes "github.com/HorizenOfficial/vela/pkg/common/crypto" commontestutil "github.com/HorizenOfficial/vela/pkg/common/testutil" @@ -190,73 +184,6 @@ func storeWasmArtifact(t *testing.T, artifactsPath string, wasmBytecode []byte) return payload } -// buildAndLoadWasmModule is a helper function to build the wasm module and read its bytecode. -func buildAndLoadWasmModule(t *testing.T) []byte { - // Get the project root directory to construct absolute paths - _, b, _, ok := runtime.Caller(0) - require.True(t, ok) - //appDir := filepath.Join(filepath.Dir(b), "../..") - projectRoot := filepath.Join(filepath.Dir(b), "../..") - appDir := filepath.Join(projectRoot, "wasm-go") - - // Build the wasm module - cmd := exec.Command("make", "build") - cmd.Dir = appDir - output, err := cmd.CombinedOutput() - require.NoError(t, err, "failed to build wasm module: %s", string(output)) - - // Load wasm bytecode for the wasm app - wasmPath := filepath.Join(appDir, "build", "payment_app.wasm") - wasmBytecode, err := os.ReadFile(wasmPath) - require.NoError(t, err) - require.NotEmpty(t, wasmBytecode) - - return wasmBytecode -} - -// uploadArtifactAndBuildDescriptorPayload uploads a WASM artifact to the local -// artifact store and returns a deploy descriptor payload (JSON) that references it. -func uploadArtifactAndBuildDescriptorPayload(t *testing.T, suite *systemTests.SystemTestSuite, wasmBytecode []byte) []byte { - t.Helper() - - store, err := deployartifact.NewStore(suite.GetArtifactsPath()) - require.NoError(t, err) - uploadAPI := deployartifact.NewAPI(store, 50, logger.NewLogger(&logger.Config{Kind: "zerolog", Console: false})) - - var body bytes.Buffer - writer := multipart.NewWriter(&body) - fileWriter, err := writer.CreateFormFile("wasm", "app.wasm") - require.NoError(t, err) - _, err = fileWriter.Write(wasmBytecode) - require.NoError(t, err) - require.NoError(t, writer.Close()) - - req := httptest.NewRequest(http.MethodPost, "/deploy/upload", &body) - req.Header.Set("Content-Type", writer.FormDataContentType()) - rr := httptest.NewRecorder() - uploadAPI.HandleUpload(rr, req) - require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) - - var uploadResp deployartifact.UploadResponse - require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &uploadResp)) - - localSum := sha256.Sum256(wasmBytecode) - localSHA := hex.EncodeToString(localSum[:]) - localArtifactID, err := common.BuildArtifactID(localSHA) - require.NoError(t, err) - require.Equal(t, localSHA, uploadResp.WasmSHA256) - require.Equal(t, localArtifactID, uploadResp.ArtifactID) - - descriptor := common.DeployDescriptor{ - Mode: common.DeployModeArtifactRef, - ArtifactID: uploadResp.ArtifactID, - WasmSHA256: uploadResp.WasmSHA256, - } - payload, err := json.Marshal(descriptor) - require.NoError(t, err) - return payload -} - // buildDeployDescriptorWithTokens uploads a WASM artifact and returns a deploy // descriptor payload (JSON) whose ConstructorParams.allowedTokens lists the // given token addresses (lowercase hex). Matches the shape the wallet's @@ -268,7 +195,7 @@ func buildDeployDescriptorWithTokens(t *testing.T, suite *systemTests.SystemTest t.Helper() // Reuse the existing uploader, then layer ConstructorParams on top. - basePayload := uploadArtifactAndBuildDescriptorPayload(t, suite, wasmBytecode) + basePayload := testhelpers.UploadArtifactAndBuildDescriptorPayload(t, suite, wasmBytecode) if len(allowedTokens) == 0 { return basePayload } @@ -311,20 +238,6 @@ func deployAppWithTokens(t *testing.T, suite *systemTests.SystemTestSuite, appID require.NoError(t, err) } -// registerSeedUser generates a user key and submits an AssociateKey request that -// embeds the encrypted secp256k1 seed — events to this user are routed via the -// hashed-subtype scheme (WaitForEventBySubtypes with AllSubtypes(seed, N)). -func registerSeedUser(t *testing.T, suite *systemTests.SystemTestSuite, cryptoHelper *systemTests.CryptoHelper, executorPubKey *cryptotypes.PublicKeyP521, appID common.ApplicationIdType, user ethCommon.Address, timeout time.Duration) { - t.Helper() - userKey, err := cryptoHelper.GenerateUserKey(user) - require.NoError(t, err) - reqID := commontestutil.GenerateRandomRequestID() - req, err := cryptoHelper.CreateAssociateKeyRequest(appID, reqID, user, userKey.PublicKey(), executorPubKey) - require.NoError(t, err) - require.NoError(t, suite.SubmitRequest(req)) - require.NoError(t, suite.AssertRequestCompleted(reqID, timeout)) -} - // registerNoSeedUser submits an AssociateKey request carrying only the 133-byte // P521 public key (no encrypted seed). The framework preserves the WASM-provided // plaintext subtype when routing events to this user. @@ -413,10 +326,10 @@ func TestPaymentAppFullFlow(t *testing.T) { // The suite accepts logger configs (not instances) so it can inject the // ephemeral log-server port into RemoteLogParams before creating the loggers. // This guarantees the zeronetwork logger connects to the correct address. - suite := systemTests.NewSystemTestSuite(t, "wasmtime-payment", newNetworkLogConfig(), newNetworkLogConfig()) + suite := systemTests.NewSystemTestSuite(t, "wasmtime-payment", testhelpers.NewNetworkLogConfig(), testhelpers.NewNetworkLogConfig()) defer suite.Cleanup() - wasmBytecode := buildAndLoadWasmModule(t) + wasmBytecode := testhelpers.BuildAndLoadWasmModule(t) require.NoError(t, suite.StartExecutor()) require.NoError(t, suite.StartManager()) @@ -441,8 +354,8 @@ func TestPaymentAppFullFlow(t *testing.T) { require.NoError(t, err) // Register user and auditor keys (both seed-registered) - registerSeedUser(t, suite, cryptoHelper, executorPubKey, appID, userAddress, timeout) - registerSeedUser(t, suite, cryptoHelper, executorPubKey, appID, auditorAddress, timeout) + testhelpers.RegisterSeedUser(t, suite, cryptoHelper, executorPubKey,appID, userAddress, timeout) + testhelpers.RegisterSeedUser(t, suite, cryptoHelper, executorPubKey,appID, auditorAddress, timeout) // Deposit 2 ETH and validate event fields (seed-registered user -> hashed subtypes) depositAmount := big.NewInt(2000000000000000000) @@ -524,10 +437,10 @@ func TestPaymentAppERC20FullFlow(t *testing.T) { t.Setenv("MANAGER_ARTIFACTS_PATH", t.TempDir()) - suite := systemTests.NewSystemTestSuite(t, "wasmtime-payment-erc20", newNetworkLogConfig(), newNetworkLogConfig()) + suite := systemTests.NewSystemTestSuite(t, "wasmtime-payment-erc20", testhelpers.NewNetworkLogConfig(), testhelpers.NewNetworkLogConfig()) defer suite.Cleanup() - wasmBytecode := buildAndLoadWasmModule(t) + wasmBytecode := testhelpers.BuildAndLoadWasmModule(t) require.NoError(t, suite.StartExecutor()) require.NoError(t, suite.StartManager()) @@ -561,8 +474,8 @@ func TestPaymentAppERC20FullFlow(t *testing.T) { // --- Register user and auditor keys --- executorPubKey, err := suite.GetExecutorCommunicationKey() require.NoError(t, err) - registerSeedUser(t, suite, cryptoHelper, executorPubKey, appID, userAddress, timeout) - registerSeedUser(t, suite, cryptoHelper, executorPubKey, appID, auditorAddress, timeout) + testhelpers.RegisterSeedUser(t, suite, cryptoHelper, executorPubKey,appID, userAddress, timeout) + testhelpers.RegisterSeedUser(t, suite, cryptoHelper, executorPubKey,appID, auditorAddress, timeout) // --- Deposit the ERC-20 token --- depositAmount := big.NewInt(1_000_000) // arbitrary; token decimals are irrelevant here @@ -610,10 +523,10 @@ func TestPaymentAppERC20MultiToken(t *testing.T) { t.Setenv("MANAGER_ARTIFACTS_PATH", t.TempDir()) - suite := systemTests.NewSystemTestSuite(t, "wasmtime-payment-erc20-multi", newNetworkLogConfig(), newNetworkLogConfig()) + suite := systemTests.NewSystemTestSuite(t, "wasmtime-payment-erc20-multi", testhelpers.NewNetworkLogConfig(), testhelpers.NewNetworkLogConfig()) defer suite.Cleanup() - wasmBytecode := buildAndLoadWasmModule(t) + wasmBytecode := testhelpers.BuildAndLoadWasmModule(t) require.NoError(t, suite.StartExecutor()) require.NoError(t, suite.StartManager()) @@ -642,8 +555,8 @@ func TestPaymentAppERC20MultiToken(t *testing.T) { // --- Register user and auditor keys --- executorPubKey, err := suite.GetExecutorCommunicationKey() require.NoError(t, err) - registerSeedUser(t, suite, cryptoHelper, executorPubKey, appID, userAddress, timeout) - registerSeedUser(t, suite, cryptoHelper, executorPubKey, appID, auditorAddress, timeout) + testhelpers.RegisterSeedUser(t, suite, cryptoHelper, executorPubKey,appID, userAddress, timeout) + testhelpers.RegisterSeedUser(t, suite, cryptoHelper, executorPubKey,appID, auditorAddress, timeout) // --- Deposit both tokens from the same user --- depositA := big.NewInt(1_000_000) @@ -713,10 +626,10 @@ func TestPaymentAppERC20MultiUser(t *testing.T) { t.Setenv("MANAGER_ARTIFACTS_PATH", t.TempDir()) - suite := systemTests.NewSystemTestSuite(t, "wasmtime-payment-erc20-multiuser", newNetworkLogConfig(), newNetworkLogConfig()) + suite := systemTests.NewSystemTestSuite(t, "wasmtime-payment-erc20-multiuser", testhelpers.NewNetworkLogConfig(), testhelpers.NewNetworkLogConfig()) defer suite.Cleanup() - wasmBytecode := buildAndLoadWasmModule(t) + wasmBytecode := testhelpers.BuildAndLoadWasmModule(t) require.NoError(t, suite.StartExecutor()) require.NoError(t, suite.StartManager()) @@ -744,8 +657,8 @@ func TestPaymentAppERC20MultiUser(t *testing.T) { // encrypted seed in the request payload). userB: no-seed (P521 key only). executorPubKey, err := suite.GetExecutorCommunicationKey() require.NoError(t, err) - registerSeedUser(t, suite, cryptoHelper, executorPubKey, appID, userA, timeout) - registerSeedUser(t, suite, cryptoHelper, executorPubKey, appID, auditorAddress, timeout) + testhelpers.RegisterSeedUser(t, suite, cryptoHelper, executorPubKey,appID, userA, timeout) + testhelpers.RegisterSeedUser(t, suite, cryptoHelper, executorPubKey,appID, auditorAddress, timeout) registerNoSeedUser(t, suite, cryptoHelper, appID, userB, timeout) // --- Both users deposit the ERC-20 token (independent amounts) --- @@ -876,10 +789,10 @@ func TestPaymentAppERC20NegativePath(t *testing.T) { t.Setenv("MANAGER_ARTIFACTS_PATH", t.TempDir()) - suite := systemTests.NewSystemTestSuite(t, "wasmtime-payment-erc20-neg", newNetworkLogConfig(), newNetworkLogConfig()) + suite := systemTests.NewSystemTestSuite(t, "wasmtime-payment-erc20-neg", testhelpers.NewNetworkLogConfig(), testhelpers.NewNetworkLogConfig()) defer suite.Cleanup() - wasmBytecode := buildAndLoadWasmModule(t) + wasmBytecode := testhelpers.BuildAndLoadWasmModule(t) require.NoError(t, suite.StartExecutor()) require.NoError(t, suite.StartManager()) @@ -907,9 +820,9 @@ func TestPaymentAppERC20NegativePath(t *testing.T) { // --- Register all three keys (all seed-registered for simplicity) --- executorPubKey, err := suite.GetExecutorCommunicationKey() require.NoError(t, err) - registerSeedUser(t, suite, cryptoHelper, executorPubKey, appID, userA, timeout) - registerSeedUser(t, suite, cryptoHelper, executorPubKey, appID, userB, timeout) - registerSeedUser(t, suite, cryptoHelper, executorPubKey, appID, auditorAddress, timeout) + testhelpers.RegisterSeedUser(t, suite, cryptoHelper, executorPubKey,appID, userA, timeout) + testhelpers.RegisterSeedUser(t, suite, cryptoHelper, executorPubKey,appID, userB, timeout) + testhelpers.RegisterSeedUser(t, suite, cryptoHelper, executorPubKey,appID, auditorAddress, timeout) // --- Baseline: userA deposits 1000 of allowedToken (succeeds) --- baselineAmount := big.NewInt(1000) @@ -1013,21 +926,6 @@ func TestPaymentAppERC20NegativePath(t *testing.T) { } } -// newNetworkLogConfig returns a zeronetwork logger config. -// The suite injects the correct log-server port into RemoteLogParams before -// creating the logger, so no hardcoded port is needed here. -func newNetworkLogConfig() *logger.Config { - return &logger.Config{ - Kind: "zeronetwork", - ConsoleColor: false, - Console: true, - ConsoleLevel: "trace", - FileLevel: "trace", - RemoteLogNetwork: "tcp", - NetworkLevel: "trace", - } -} - func newConsoleLogConfig() *logger.Config { return &logger.Config{ Kind: "zerolog", diff --git a/runtime/wasm-go/testhelpers/helpers.go b/runtime/wasm-go/testhelpers/helpers.go new file mode 100644 index 0000000..61ed477 --- /dev/null +++ b/runtime/wasm-go/testhelpers/helpers.go @@ -0,0 +1,163 @@ +// Package testhelpers provides helpers shared between vela-nova's system_tests +// (mock suite) and fullstack_tests (real-chain suite). Before this package +// existed, each test folder owned local copies of these helpers; they drifted +// over time and fixes in one folder didn't reach the other. +// +// Helpers that need to drive the suite accept a DeploySuite — a minimal +// interface satisfied by both *testutil.SystemTestSuite and +// *fullstack.FullStackSystemTestSuite. Helpers that do not need the suite +// (WASM build, log config) are plain functions. +package testhelpers + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "mime/multipart" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "path/filepath" + "runtime" + "testing" + "time" + + "github.com/HorizenOfficial/vela/pkg/authorityservice/deployartifact" + "github.com/HorizenOfficial/vela/pkg/common" + cryptotypes "github.com/HorizenOfficial/vela/pkg/common/crypto" + commontestutil "github.com/HorizenOfficial/vela/pkg/common/testutil" + "github.com/HorizenOfficial/vela/pkg/logger" + "github.com/HorizenOfficial/vela/pkg/testutil" + "github.com/stretchr/testify/require" + ethCommon "github.com/ethereum/go-ethereum/common" +) + +// DeploySuite is the minimal behavior the shared helpers need from a test +// suite. Both *testutil.SystemTestSuite (mock) and +// *fullstack.FullStackSystemTestSuite satisfy it via methods on their embedded +// *testutil.TestSuiteCore plus their own SubmitRequest/AssertRequestCompleted +// implementations. +// +// A narrower split (one interface per helper) was considered and rejected: +// both suites implement all three methods, and fragmenting adds noise without +// real decoupling benefit. If a future helper genuinely needs only a subset, +// it can declare its own inline interface — Go satisfies them implicitly. +type DeploySuite interface { + GetArtifactsPath() string + SubmitRequest(*common.Request) error + AssertRequestCompleted(common.RequestIdType, time.Duration) error +} + +// BuildAndLoadWasmModule builds the payment-app WASM via `make build` and +// returns the compiled bytecode. Invokes TinyGo; expects the runtime/wasm-go +// directory to be reachable via ../.. from this package file. +func BuildAndLoadWasmModule(t *testing.T) []byte { + t.Helper() + _, thisFile, _, ok := runtime.Caller(0) + require.True(t, ok) + // This file lives at runtime/wasm-go/testhelpers/helpers.go — the WASM + // source is one directory up (runtime/wasm-go). + appDir := filepath.Join(filepath.Dir(thisFile), "..") + + cmd := exec.Command("make", "build") + cmd.Dir = appDir + output, err := cmd.CombinedOutput() + require.NoError(t, err, "failed to build wasm module: %s", string(output)) + + wasmPath := filepath.Join(appDir, "build", "payment_app.wasm") + wasmBytecode, err := os.ReadFile(wasmPath) + require.NoError(t, err) + require.NotEmpty(t, wasmBytecode) + + return wasmBytecode +} + +// UploadArtifactAndBuildDescriptorPayload writes the WASM bytes to the suite's +// deploy-artifact store and returns a deploy descriptor (JSON) that references +// the stored artifact by SHA-256. Matches the shape the manager expects on +// deploy request processing. +func UploadArtifactAndBuildDescriptorPayload(t *testing.T, suite DeploySuite, wasmBytecode []byte) []byte { + t.Helper() + + store, err := deployartifact.NewStore(suite.GetArtifactsPath()) + require.NoError(t, err) + uploadAPI := deployartifact.NewAPI(store, 50, logger.NewLogger(&logger.Config{Kind: "zerolog", Console: false})) + + var body bytes.Buffer + writer := multipart.NewWriter(&body) + fileWriter, err := writer.CreateFormFile("wasm", "app.wasm") + require.NoError(t, err) + _, err = fileWriter.Write(wasmBytecode) + require.NoError(t, err) + require.NoError(t, writer.Close()) + + req := httptest.NewRequest(http.MethodPost, "/deploy/upload", &body) + req.Header.Set("Content-Type", writer.FormDataContentType()) + rr := httptest.NewRecorder() + uploadAPI.HandleUpload(rr, req) + require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) + + var uploadResp deployartifact.UploadResponse + require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &uploadResp)) + + localSum := sha256.Sum256(wasmBytecode) + localSHA := hex.EncodeToString(localSum[:]) + localArtifactID, err := common.BuildArtifactID(localSHA) + require.NoError(t, err) + require.Equal(t, localSHA, uploadResp.WasmSHA256) + require.Equal(t, localArtifactID, uploadResp.ArtifactID) + + descriptor := common.DeployDescriptor{ + Mode: common.DeployModeArtifactRef, + ArtifactID: uploadResp.ArtifactID, + WasmSHA256: uploadResp.WasmSHA256, + } + payload, err := json.Marshal(descriptor) + require.NoError(t, err) + return payload +} + +// RegisterSeedUser generates a user P521 key via the CryptoHelper, submits an +// AssociateKey request that carries the encrypted seed, and waits for +// completion. After registration the user's events can be retrieved via +// WaitForEventBySubtypes with the hashed-subtype scheme. +// +// Uses req.RequestID after SubmitRequest so the helper works against both +// suite variants: the mock suite leaves req.RequestID untouched; the +// fullstack suite overwrites it with the contract-assigned ID. +func RegisterSeedUser( + t *testing.T, + suite DeploySuite, + cryptoHelper *testutil.CryptoHelper, + executorPubKey *cryptotypes.PublicKeyP521, + appID common.ApplicationIdType, + user ethCommon.Address, + timeout time.Duration, +) { + t.Helper() + userKey, err := cryptoHelper.GenerateUserKey(user) + require.NoError(t, err) + reqID := commontestutil.GenerateRandomRequestID() + req, err := cryptoHelper.CreateAssociateKeyRequest(appID, reqID, user, userKey.PublicKey(), executorPubKey) + require.NoError(t, err) + require.NoError(t, suite.SubmitRequest(req)) + require.NoError(t, suite.AssertRequestCompleted(req.RequestID, timeout)) +} + +// NewNetworkLogConfig returns a logger.Config configured for the zeronetwork +// kind (logs forwarded to the in-test log server). The suite injects the +// correct log-server port into RemoteLogParams before creating the logger, so +// no hardcoded port is needed here. +func NewNetworkLogConfig() *logger.Config { + return &logger.Config{ + Kind: "zeronetwork", + ConsoleColor: false, + Console: true, + ConsoleLevel: "trace", + FileLevel: "trace", + RemoteLogNetwork: "tcp", + NetworkLevel: "trace", + } +} diff --git a/wallet/testutil/wallet_driver.go b/wallet/testutil/wallet_driver.go index acf7792..1366886 100644 --- a/wallet/testutil/wallet_driver.go +++ b/wallet/testutil/wallet_driver.go @@ -21,7 +21,10 @@ package testutil import ( + "fmt" + "os" "path/filepath" + "strings" "testing" "github.com/HorizenOfficial/vela-nova/wallet/app" @@ -178,3 +181,34 @@ func (d *WalletDriver) ConfPath() string { func (d *WalletDriver) NewBlockchainClient() blockchain.Client { return d.newUserClient() } + +// AddToken appends an ERC-20 entry to the wallet.conf file so subsequent +// wrapper calls can resolve the symbol (e.g. Deposit(..., "MOCK", ...)). The +// driver does not mutate an in-memory registry — each wrapper re-loads the +// conf via LoadConfigFromFile, which rebuilds the registry from the current +// file contents through LoadTokenRegistry (the same path a real CLI takes). +// +// Symbols are upper-cased on write, matching how the config parser stores +// and looks them up. The zero address is rejected by LoadTokenRegistry (it +// is reserved for ETH); surface that as an immediate error so tests don't +// fail in a downstream wrapper with a confusing message. +func (d *WalletDriver) AddToken(symbol string, address ethCommon.Address, decimals uint8) { + d.t.Helper() + require.NotEmpty(d.t, symbol, "AddToken: symbol must not be empty") + require.NotEqual(d.t, ethCommon.Address{}, address, "AddToken: zero address is reserved for ETH") + + upperSym := strings.ToUpper(symbol) + entry := fmt.Sprintf("token.%s.address=%s\ntoken.%s.decimals=%d\n", upperSym, address.Hex(), upperSym, decimals) + + f, err := os.OpenFile(d.confPath, os.O_APPEND|os.O_WRONLY, 0o644) + require.NoError(d.t, err, "AddToken: open conf") + defer f.Close() + _, err = f.WriteString(entry) + require.NoError(d.t, err, "AddToken: append conf") + + // Re-load now so any malformed entry fails here instead of during the + // next wrapper call. Also catches collisions (duplicate symbol or address) + // that LoadTokenRegistry would otherwise reject mid-flow. + _, err = app.LoadConfigFromFile(d.confPath) + require.NoError(d.t, err, "AddToken: re-load conf after append") +} From 4754a734f39b0db1ca9ecb0d4e377ce36e77bea0 Mon Sep 17 00:00:00 2001 From: Alberto Sala Date: Wed, 22 Apr 2026 17:51:13 +0200 Subject: [PATCH 08/25] Aded erc20 main test --- .../fullstack_tests/erc20_fullstack_test.go | 159 ++++++++++++++++++ runtime/wasm-go/go.mod | 8 +- runtime/wasm-go/go.sum | 10 +- 3 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 runtime/wasm-go/fullstack_tests/erc20_fullstack_test.go diff --git a/runtime/wasm-go/fullstack_tests/erc20_fullstack_test.go b/runtime/wasm-go/fullstack_tests/erc20_fullstack_test.go new file mode 100644 index 0000000..2afb943 --- /dev/null +++ b/runtime/wasm-go/fullstack_tests/erc20_fullstack_test.go @@ -0,0 +1,159 @@ +package main_test + +import ( + "math/big" + "os" + "path/filepath" + "testing" + + "github.com/HorizenOfficial/vela-nova/payment-app/testhelpers" + walletTestutil "github.com/HorizenOfficial/vela-nova/wallet/testutil" + "github.com/HorizenOfficial/vela/pkg/testutil/fullstack" + "github.com/stretchr/testify/require" +) + +// TestERC20FullStack is the Phase 5 reference test: a full end-to-end ERC-20 +// flow that exercises every layer of the harness (real simulated chain, real +// Wasmtime payment-app, in-process subgraph, in-process authority, wallet +// driver). The flow mirrors the plan's 14 steps: +// +// 1. Start the suite. +// 2. Deploy MockERC20. +// 3. Allowlist it on ProcessorEndpoint. +// 4. Start manager + executor. +// 5. Build wallet driver (creates funded user, grants DEPLOYER_ROLE). +// 6. driver.AddToken("MOCK", mockAddr, 18) — adds the token to wallet.conf. +// 7. driver.DeployApp(wasmPath, ["MOCK"]) — wallet-driven deploy. +// 8. driver.RegisterUser(). +// 9. Mint MockERC20 to the user (test-side fixture). +// 10. suite.ApproveERC20(user -> processor, amount) — option A: pre-approve +// before driver.Deposit, since the wallet's current submitRequest path +// does not embed EIP-2612 permits. +// 11. driver.Deposit("1000", "MOCK"). Assert on-chain appCustody += 1000 MOCK. +// 12. driver.GetPrivateBalance("MOCK") → 1000 MOCK (in raw units). +// 13. driver.Withdraw("400", userPublicAddr, "MOCK"). +// 14. Assert pendingClaims[MOCK][user] == 400 MOCK. +// 15. driver.ClaimPendingPayments("MOCK"). Assert user's on-chain MOCK balance +// increased by 400. +// 16. driver.GetPrivateBalance("MOCK") → 600 MOCK remaining. +func TestERC20FullStack(t *testing.T) { + if os.Getenv("CI_FLAG") != "" { + t.Skip("Skipping fullstack test in CI environment") + } + + t.Setenv("MANAGER_ARTIFACTS_PATH", t.TempDir()) + + suite := fullstack.NewFullStackSystemTestSuite(t, "wasmtime-payment", + testhelpers.NewNetworkLogConfig(), testhelpers.NewNetworkLogConfig()) + defer suite.Cleanup() + + sim := suite.GetSimTestHelper() + + // Step 2: deploy MockERC20. 18 decimals matches most real ERC-20s and + // makes raw-unit math match wei conversions. + mockAddr := sim.DeployMockERC20("Mock Token", "MOCK", 18) + t.Logf("MockERC20 deployed at %s", mockAddr.Hex()) + + // Step 3: allowlist on ProcessorEndpoint (ADMIN-role'd call from Deployer). + sim.WaitMined(sim.AddAllowedToken(mockAddr)) + + // Step 4: start executor + manager (order matters — manager handshakes with executor). + require.NoError(t, suite.StartExecutor()) + require.NoError(t, suite.StartManager()) + + // Step 5: wallet driver — creates funded user, grants DEPLOYER_ROLE. + driver := walletTestutil.NewWalletDriver(t, suite) + + // Step 6: teach the wallet about MOCK by appending to wallet.conf. + // Subsequent driver wrappers re-load the conf and resolve "MOCK" via + // LoadTokenRegistry. + driver.AddToken("MOCK", mockAddr, 18) + + // Step 7: deploy the payment-app WASM with MOCK allowlisted at the guest level + // (ConstructorParams.allowedTokens). The wallet's deployapp resolves "MOCK" + // via the registry entry we just added. + wasmBytes := testhelpers.BuildAndLoadWasmModule(t) + wasmPath := filepath.Join(t.TempDir(), "payment_app.wasm") + require.NoError(t, os.WriteFile(wasmPath, wasmBytes, 0o644)) + + appID, err := driver.DeployApp(t.Context(), wasmPath, "100 wei", []string{"MOCK"}) + require.NoError(t, err) + require.NotZero(t, appID, "DeployApp must assign a non-zero ApplicationID") + t.Logf("Deployed application %d", appID) + + // Step 8: register the user's P521 key so the enclave can encrypt events. + require.NoError(t, driver.RegisterUser(t.Context(), "100 wei")) + + // Step 9: mint 10,000 MOCK to the user (test-side fixture — MockERC20's mint + // is permissionless). + mockUnit := new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil) // 10^18 + mintAmount := new(big.Int).Mul(big.NewInt(10000), mockUnit) + user := driver.UserAddress() + sim.WaitMined(sim.MintERC20(user, mintAmount)) + require.Equal(t, 0, mintAmount.Cmp(sim.BalanceOfERC20(user)), + "user should hold the minted MOCK before deposit") + + // Step 10: pre-approve the ProcessorEndpoint to spend the user's MOCK. + // Option A of the Phase 5 flow — the wallet's submitRequest uses + // transferFrom and does not embed an EIP-2612 permit, so approval must be + // pre-staged. A permit-embedding wallet (option B) is future work. + userOpts, err := suite.GetTransactOpts(user) + require.NoError(t, err) + sim.WaitMined(sim.ApproveERC20(userOpts, sim.ProcessorContractAddress, mintAmount)) + + // Step 11: deposit 1000 MOCK. Amount is a decimal string (wallet parses + // with token decimals). + depositRaw := new(big.Int).Mul(big.NewInt(1000), mockUnit) + require.NoError(t, driver.Deposit(t.Context(), "1000", "MOCK", "100 wei")) + + custody := suite.GetAppCustody(appID, mockAddr) + require.Equal(t, 0, depositRaw.Cmp(custody), + "on-chain appCustody should equal deposited amount (1000 MOCK); got %s", custody.String()) + + // Step 12: the wallet's private balance view decrypts the user's deposit + // events and reports the running balance in raw token units. + privBal, err := driver.GetPrivateBalance(t.Context(), "MOCK") + require.NoError(t, err) + require.NotNil(t, privBal, "GetPrivateBalance returned nil (no matching event found)") + require.Equal(t, 0, depositRaw.Cmp(privBal), + "private balance after deposit should equal deposited amount; got %s", privBal.String()) + + // Step 13: withdraw 400 MOCK back to the user's public address. + withdrawRaw := new(big.Int).Mul(big.NewInt(400), mockUnit) + require.NoError(t, driver.Withdraw(t.Context(), "400", user.Hex(), "MOCK", "100 wei")) + + // Step 14: pendingClaims accumulates the withdrawn amount until Claim is called. + pending := sim.GetPendingClaims(mockAddr, user) + require.Equal(t, 0, withdrawRaw.Cmp(pending), + "pendingClaims[MOCK][user] should equal the withdraw amount; got %s", pending.String()) + + // Step 15: claim moves the pending amount to the user's ERC-20 balance. + // Before claim, the user holds (mint - deposit) MOCK on-chain. After claim, + // that should increase by the withdrawn amount. + balBeforeClaim := sim.BalanceOfERC20(user) + expectedSpent := depositRaw // everything we deposited is off the user's on-chain balance + require.Equal(t, 0, new(big.Int).Sub(mintAmount, expectedSpent).Cmp(balBeforeClaim), + "pre-claim balance sanity check failed") + + require.NoError(t, driver.ClaimPendingPayments(t.Context(), "MOCK")) + + balAfterClaim := sim.BalanceOfERC20(user) + expectedBalAfterClaim := new(big.Int).Add(balBeforeClaim, withdrawRaw) + require.Equal(t, 0, expectedBalAfterClaim.Cmp(balAfterClaim), + "post-claim balance should include the claimed withdraw; got %s", balAfterClaim.String()) + + // pendingClaims should be zeroed after the claim. + pendingAfter := sim.GetPendingClaims(mockAddr, user) + require.Equal(t, 0, big.NewInt(0).Cmp(pendingAfter), + "pendingClaims should be zero after claim; got %s", pendingAfter.String()) + + // Step 16: private balance should now be (deposit - withdraw) = 600 MOCK. + remainingRaw := new(big.Int).Sub(depositRaw, withdrawRaw) + privBal2, err := driver.GetPrivateBalance(t.Context(), "MOCK") + require.NoError(t, err) + require.NotNil(t, privBal2) + require.Equal(t, 0, remainingRaw.Cmp(privBal2), + "remaining private balance should be 600 MOCK; got %s", privBal2.String()) + + t.Log("Phase 5 ERC-20 full-stack flow passed") +} diff --git a/runtime/wasm-go/go.mod b/runtime/wasm-go/go.mod index fa4a34e..335fd83 100644 --- a/runtime/wasm-go/go.mod +++ b/runtime/wasm-go/go.mod @@ -11,9 +11,12 @@ replace github.com/HorizenOfficial/vela => ../../../vela //replace github.com/HorizenOfficial/vela-common-go => ../../../vela-common-go +replace github.com/HorizenOfficial/vela-nova/wallet => ../../wallet + require ( github.com/HorizenOfficial/vela v0.0.28 github.com/HorizenOfficial/vela-common-go v0.0.28 + github.com/HorizenOfficial/vela-nova/wallet v0.0.0-00010101000000-000000000000 github.com/ethereum/go-ethereum v1.17.2 github.com/stretchr/testify v1.11.1 ) @@ -49,7 +52,7 @@ require ( github.com/cockroachdb/redact v1.1.5 // indirect github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect github.com/consensys/gnark-crypto v0.18.1 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dchest/siphash v1.2.3 // indirect @@ -82,6 +85,7 @@ require ( github.com/holiman/bloomfilter/v2 v2.0.3 // indirect github.com/holiman/uint256 v1.3.2 // indirect github.com/huin/goupnp v1.3.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/klauspost/compress v1.17.8 // indirect github.com/klauspost/cpuid/v2 v2.0.9 // indirect @@ -112,6 +116,8 @@ require ( github.com/rs/zerolog v1.34.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect + github.com/spf13/cobra v1.9.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect github.com/supranational/blst v0.3.16 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect diff --git a/runtime/wasm-go/go.sum b/runtime/wasm-go/go.sum index 0a952ce..fd16f01 100644 --- a/runtime/wasm-go/go.sum +++ b/runtime/wasm-go/go.sum @@ -67,8 +67,8 @@ github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1: github.com/consensys/gnark-crypto v0.18.1 h1:RyLV6UhPRoYYzaFnPQA4qK3DyuDgkTgskDdoGqFt3fI= github.com/consensys/gnark-crypto v0.18.1/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= -github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc= github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -169,6 +169,8 @@ github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXei github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/influxdata/influxdb-client-go/v2 v2.4.0 h1:HGBfZYStlx3Kqvsv1h2pJixbCl/jhnFtxpKFAv9Tu5k= github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8= github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSHzRbhzK8RdXOsAdfDgO49TtqC1oZ+acxPrkfTxcCs= @@ -268,6 +270,10 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= From cf9e1ca1ff6451c959aeedf50ded79806887a5cb Mon Sep 17 00:00:00 2001 From: Alberto Sala Date: Thu, 23 Apr 2026 14:36:13 +0200 Subject: [PATCH 09/25] Added more tests full stack path --- .../fullstack_tests/deanonymize_test.go | 133 ++++++++++++++ .../fullstack_tests/erc20_unlisted_test.go | 73 ++++++++ .../private_transfer_erc20_test.go | 171 ++++++++++++++++++ .../fullstack_tests/private_transfer_test.go | 109 +++++++++++ 4 files changed, 486 insertions(+) create mode 100644 runtime/wasm-go/fullstack_tests/deanonymize_test.go create mode 100644 runtime/wasm-go/fullstack_tests/erc20_unlisted_test.go create mode 100644 runtime/wasm-go/fullstack_tests/private_transfer_erc20_test.go create mode 100644 runtime/wasm-go/fullstack_tests/private_transfer_test.go diff --git a/runtime/wasm-go/fullstack_tests/deanonymize_test.go b/runtime/wasm-go/fullstack_tests/deanonymize_test.go new file mode 100644 index 0000000..1548433 --- /dev/null +++ b/runtime/wasm-go/fullstack_tests/deanonymize_test.go @@ -0,0 +1,133 @@ +package main_test + +import ( + "encoding/json" + "fmt" + "math/big" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/HorizenOfficial/vela-nova/payment-app/testhelpers" + walletTestutil "github.com/HorizenOfficial/vela-nova/wallet/testutil" + "github.com/HorizenOfficial/vela/pkg/testutil/fullstack" + "github.com/stretchr/testify/require" +) + +// TestDeanonymize_RealRoundTrip exercises the full authority deanonymization +// pipeline against real components — no synthetic injection. It is the +// end-to-end counterpart to Phase 3's /nonce smoke test, which only +// verified the HTTP service boot and signature check against a hand-written +// report file. +// +// Flow: +// 1. Two wallet drivers: user (deposits ETH so there's something to report) +// and auth (acts as the authority — registered user + on-chain authority). +// 2. Both RegisterUser so the executor has both P521 pubkeys in its +// keyStore (required for encrypting the report to auth). +// 3. suite.RegisterAuthority grants auth's address DEFAULT_AUTHORITY role +// for the app — otherwise the on-chain Deanonymize submit reverts. +// 4. auth.RequestReport submits a Deanonymize request. The manager picks +// it up, the WASM guest returns report bytes, the executor encrypts +// those bytes to auth's P521 pubkey, and the manager writes the resulting +// DeanonymizationReport JSON to the reports dir. +// 5. auth.DownloadReport runs the /nonce + /getreport + decrypt flow +// against the in-process authority service, writing the decrypted +// report to a destination file. +// 6. The test reads that file and asserts the decrypted content carries +// both the expected ApplicationID/RequestID AND a non-empty reportData +// payload that references the user's address. This is what catches +// regressions in any layer — subgraph indexing of the RequestCompleted +// event, real executor-side encryption-to-requester, or HTTP boundary +// signature verification. +func TestDeanonymize_RealRoundTrip(t *testing.T) { + if os.Getenv("CI_FLAG") != "" { + t.Skip("Skipping fullstack test in CI environment") + } + + t.Setenv("MANAGER_ARTIFACTS_PATH", t.TempDir()) + + suite := fullstack.NewFullStackSystemTestSuite(t, "wasmtime-payment", + testhelpers.NewNetworkLogConfig(), testhelpers.NewNetworkLogConfig()) + defer suite.Cleanup() + + require.NoError(t, suite.StartExecutor()) + require.NoError(t, suite.StartManager()) + + driverUser := walletTestutil.NewWalletDriver(t, suite) + driverAuth := walletTestutil.NewWalletDriver(t, suite) + + wasmBytes := testhelpers.BuildAndLoadWasmModule(t) + wasmPath := filepath.Join(t.TempDir(), "payment_app.wasm") + require.NoError(t, os.WriteFile(wasmPath, wasmBytes, 0o644)) + + appID, err := driverUser.DeployApp(t.Context(), wasmPath, "100 wei", nil) + require.NoError(t, err) + require.NotZero(t, appID) + driverAuth.SetApplicationID(appID) + + // Both wallets register so the executor keyStore has their P521 pubkeys. + // Without auth's P521 entry, the executor's + // encryptDeanonymizationReport call would fail with + // CodePubKeyNotRegistered. + require.NoError(t, driverUser.RegisterUser(t.Context(), "100 wei")) + require.NoError(t, driverAuth.RegisterUser(t.Context(), "100 wei")) + + // User has some balance to report on. + require.NoError(t, driverUser.Deposit(t.Context(), "1 ETH", "", "100 wei")) + + // Grant auth's on-chain address the DEFAULT_AUTHORITY role for this + // application — otherwise the Deanonymize submitRequest reverts with + // AuthorityNotAllowed. + suite.RegisterAuthority(appID) + // suite.RegisterAuthority grants the *in-process authority* (Phase 3 + // key) the default-authority role, but our driverAuth is a different + // address. Grant it too via the lower-level helper. + sim := suite.GetSimTestHelper() + sim.WaitMined(sim.AddAuthority(new(big.Int).SetUint64(uint64(appID)), driverAuth.UserAddress())) + + // Auth submits the Deanonymize request. Default report type = "balances". + reportID, err := driverAuth.RequestReport(t.Context(), "balances", "100 wei") + require.NoError(t, err) + + // Report is now persisted to the manager's reports dir (same path the + // authority service reads from — Phase 3 wiring) and the subgraph has + // the RequestCompleted entry for the /getreport endpoint's sanity check. + // Pull it down via the full HTTP + decrypt pipeline. + reportIDHex := fmt.Sprintf("%x", reportID[:]) + t.Logf("deanonymization request id: %s", reportIDHex) + destPath := filepath.Join(t.TempDir(), "report.json") + require.NoError(t, driverAuth.DownloadReport(t.Context(), reportIDHex, destPath)) + + raw, err := os.ReadFile(destPath) + require.NoError(t, err) + require.NotEmpty(t, raw) + + // The decrypted report's shape is what downloadreport writes: + // applicationId and requestId are string-encoded (their custom + // MarshalJSON), reportData is the raw payload (JSON or base64 fallback). + var got struct { + ApplicationID string `json:"applicationId"` + RequestID string `json:"requestId"` + ReportData json.RawMessage `json:"reportData"` + } + require.NoError(t, json.Unmarshal(raw, &got)) + + require.Equal(t, appID.String(), got.ApplicationID, + "decrypted report should reference the deployed app") + // RequestIdType.MarshalJSON adds "0x" prefix; our reportIDHex is the + // unprefixed form used as the downloadreport flag value. + require.Equal(t, "0x"+reportIDHex, got.RequestID, + "decrypted report should reference the request that produced it") + require.NotEmpty(t, got.ReportData, "reportData should be populated for a balances report") + + // Balances reports include the user's address somewhere in the payload. + // Compare case-insensitively — the payment app emits lowercase hex while + // UserAddress() returns the EIP-55 checksum form. + require.Contains(t, strings.ToLower(string(got.ReportData)), + strings.ToLower(driverUser.UserAddress().Hex()), + "balances report should mention the depositing user; raw=%s", string(got.ReportData)) + + t.Log("Real deanonymize /getreport round-trip verified") +} diff --git a/runtime/wasm-go/fullstack_tests/erc20_unlisted_test.go b/runtime/wasm-go/fullstack_tests/erc20_unlisted_test.go new file mode 100644 index 0000000..825388d --- /dev/null +++ b/runtime/wasm-go/fullstack_tests/erc20_unlisted_test.go @@ -0,0 +1,73 @@ +package main_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/HorizenOfficial/vela-nova/payment-app/testhelpers" + walletTestutil "github.com/HorizenOfficial/vela-nova/wallet/testutil" + "github.com/HorizenOfficial/vela/pkg/testutil/fullstack" + "github.com/stretchr/testify/require" +) + +// TestERC20UnlistedTokenReverts asserts that depositing an ERC-20 that has +// NOT been added to the ProcessorEndpoint's token allowlist reverts on-chain +// with the contract's TokenNotAllowed error. This is the critical +// security gate keeping unvetted ERC-20s out of the protocol — a regression +// here would let anyone deposit arbitrary tokens. +// +// The test skips suite.AddAllowedToken deliberately. The guest-side +// allowlist (DeployApp's allowedTokens) doesn't matter: the contract check +// runs first and short-circuits the request before any WASM execution. +func TestERC20UnlistedTokenReverts(t *testing.T) { + if os.Getenv("CI_FLAG") != "" { + t.Skip("Skipping fullstack test in CI environment") + } + + t.Setenv("MANAGER_ARTIFACTS_PATH", t.TempDir()) + + suite := fullstack.NewFullStackSystemTestSuite(t, "wasmtime-payment", + testhelpers.NewNetworkLogConfig(), testhelpers.NewNetworkLogConfig()) + defer suite.Cleanup() + + sim := suite.GetSimTestHelper() + + // Deploy MockERC20 but INTENTIONALLY do not allowlist it. + mockAddr := sim.DeployMockERC20("Unlisted Token", "UNL", 18) + t.Logf("MockERC20 deployed at %s (not allowlisted on ProcessorEndpoint)", mockAddr.Hex()) + + require.NoError(t, suite.StartExecutor()) + require.NoError(t, suite.StartManager()) + + driver := walletTestutil.NewWalletDriver(t, suite) + + // Tell the wallet about UNL so the symbol resolves. Without this the + // deposit would fail at ResolveToken with "unknown token" — a different + // error path from what we want to exercise. + driver.AddToken("UNL", mockAddr, 18) + + // Deploy the payment-app. Listing UNL here only affects the guest-side + // allowlist; the contract-side gate is independent. + wasmBytes := testhelpers.BuildAndLoadWasmModule(t) + wasmPath := filepath.Join(t.TempDir(), "payment_app.wasm") + require.NoError(t, os.WriteFile(wasmPath, wasmBytes, 0o644)) + + appID, err := driver.DeployApp(t.Context(), wasmPath, "100 wei", []string{"UNL"}) + require.NoError(t, err) + require.NotZero(t, appID) + + require.NoError(t, driver.RegisterUser(t.Context(), "100 wei")) + + // Attempt the deposit. No mint or approve is needed — the contract's + // allowlist check runs before _pullERC20, so the revert happens without + // touching the token contract. + err = driver.Deposit(t.Context(), "100", "UNL", "100 wei") + require.Error(t, err, "deposit of unlisted token must return an error") + + // The exact wrapping depends on how go-ethereum surfaces the revert + // reason through the simulated backend. At minimum the error should + // reference the token-allowlist revert so callers can tell why it failed. + require.Contains(t, err.Error(), "TokenNotAllowed", + "expected TokenNotAllowed revert reason in error; got: %v", err) +} diff --git a/runtime/wasm-go/fullstack_tests/private_transfer_erc20_test.go b/runtime/wasm-go/fullstack_tests/private_transfer_erc20_test.go new file mode 100644 index 0000000..da12e4c --- /dev/null +++ b/runtime/wasm-go/fullstack_tests/private_transfer_erc20_test.go @@ -0,0 +1,171 @@ +package main_test + +import ( + "math/big" + "os" + "path/filepath" + "testing" + + "github.com/HorizenOfficial/vela-nova/payment-app/testhelpers" + walletTestutil "github.com/HorizenOfficial/vela-nova/wallet/testutil" + "github.com/HorizenOfficial/vela/pkg/testutil/fullstack" + "github.com/stretchr/testify/require" +) + +// TestPrivateTransfer_TwoUsers_ERC20 is the ERC-20 + multi-token sibling of +// TestPrivateTransfer_TwoUsers. Beyond re-running the two-user private +// transfer flow with an ERC-20 asset, it pins down three things that the +// ETH-only test and the single-token TestERC20FullStack cannot catch alone: +// +// 1. parseAssetAmount correctness for non-18 decimals. MOCK is deployed with +// decimals=6 (USDC-like). The wallet must interpret "1000" as 1000*10^6 +// raw, not 1000*10^18 — any hardcoded 18-decimal assumption on the ERC-20 +// path would blow up here. +// +// 2. Per-token balance isolation on a single user. User A privately holds +// both 1 ETH and 1000 MOCK. After A transfers 300 MOCK to B, A's MOCK +// balance reads 700 (in raw 10^6 units) and A's ETH balance reads 1 ETH +// UNCHANGED. If getprivatebalance's tokenAddress filter (or the +// wallet-side decrypt) cross-contaminated across tokens, this assertion +// would fail. +// +// 3. Per-user event routing for ERC-20 events. B sees 300 MOCK; B's ETH +// reads nil (B never received ETH, even though A holds some). This +// extends the ETH-only routing check to events carrying a non-zero +// tokenAddress. +// +// Flow (12 steps): +// +// 1. Suite start; deploy MockERC20("MOCK", decimals=6); allowlist on +// ProcessorEndpoint; start executor + manager. +// 2. Two drivers A, B; both AddToken("MOCK", mockAddr, 6). +// 3. A deploys the payment-app with MOCK in allowedTokens; B.SetApplicationID(appID). +// 4. Both RegisterUser. +// 5. Mint 10,000 MOCK to A; pre-approve processor for 10,000 MOCK (option-A +// pattern — wallet does not embed EIP-2612 permits). +// 6. A.Deposit("1000", "MOCK") → assert appCustody[MOCK] == 1000*10^6. +// 7. A.Deposit("1 ETH", "") → gives A a parallel ETH balance. +// 8. A.GetPrivateBalance("MOCK") == 1000*10^6 and A.GetPrivateBalance("") == 1 ETH. +// 9. A.PrivateTransfer(B, "300", "MOCK"). +// 10. A.GetPrivateBalance("MOCK") == 700*10^6 AND A.GetPrivateBalance("") == 1 ETH +// (the multi-token isolation assertion — ETH untouched by the MOCK transfer). +// 11. B.GetPrivateBalance("MOCK") == 300*10^6. +// 12. B.GetPrivateBalance("") == nil (B never received ETH). +func TestPrivateTransfer_TwoUsers_ERC20(t *testing.T) { + if os.Getenv("CI_FLAG") != "" { + t.Skip("Skipping fullstack test in CI environment") + } + + t.Setenv("MANAGER_ARTIFACTS_PATH", t.TempDir()) + + suite := fullstack.NewFullStackSystemTestSuite(t, "wasmtime-payment", + testhelpers.NewNetworkLogConfig(), testhelpers.NewNetworkLogConfig()) + defer suite.Cleanup() + + sim := suite.GetSimTestHelper() + + // Step 1: 6-decimal MOCK is the whole point of this variant — USDC-like, + // catches any 10^18 assumption baked into the wallet ERC-20 path. + const mockDecimals = 6 + mockAddr := sim.DeployMockERC20("Mock Token", "MOCK", mockDecimals) + sim.WaitMined(sim.AddAllowedToken(mockAddr)) + + require.NoError(t, suite.StartExecutor()) + require.NoError(t, suite.StartManager()) + + // Step 2: two drivers; both learn about MOCK so either can resolve the + // symbol on deposit / transfer / balance lookups. + driverA := walletTestutil.NewWalletDriver(t, suite) + driverB := walletTestutil.NewWalletDriver(t, suite) + driverA.AddToken("MOCK", mockAddr, mockDecimals) + driverB.AddToken("MOCK", mockAddr, mockDecimals) + + // Step 3: A deploys with MOCK allowlisted at the guest level. + wasmBytes := testhelpers.BuildAndLoadWasmModule(t) + wasmPath := filepath.Join(t.TempDir(), "payment_app.wasm") + require.NoError(t, os.WriteFile(wasmPath, wasmBytes, 0o644)) + + appID, err := driverA.DeployApp(t.Context(), wasmPath, "100 wei", []string{"MOCK"}) + require.NoError(t, err) + require.NotZero(t, appID) + driverB.SetApplicationID(appID) + + // Step 4: both register — seeds the per-user event routing. + require.NoError(t, driverA.RegisterUser(t.Context(), "100 wei")) + require.NoError(t, driverB.RegisterUser(t.Context(), "100 wei")) + + // Step 5: mint MOCK to A and pre-approve the processor. Option-A pattern + // (permit-embedding wallet is future work). + mockUnit := new(big.Int).Exp(big.NewInt(10), big.NewInt(mockDecimals), nil) // 10^6 + mintAmount := new(big.Int).Mul(big.NewInt(10000), mockUnit) + userA := driverA.UserAddress() + sim.WaitMined(sim.MintERC20(userA, mintAmount)) + + userAOpts, err := suite.GetTransactOpts(userA) + require.NoError(t, err) + sim.WaitMined(sim.ApproveERC20(userAOpts, sim.ProcessorContractAddress, mintAmount)) + + // Step 6: deposit 1000 MOCK. The wallet parses "1000" with decimals=6 + // from the registry entry we just added. + depositMockRaw := new(big.Int).Mul(big.NewInt(1000), mockUnit) + require.NoError(t, driverA.Deposit(t.Context(), "1000", "MOCK", "100 wei")) + require.Equal(t, 0, depositMockRaw.Cmp(suite.GetAppCustody(appID, mockAddr)), + "appCustody[MOCK] should equal 1000 MOCK after deposit") + + // Step 7: deposit 1 ETH. A now holds two assets privately. + oneEther := new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil) + require.NoError(t, driverA.Deposit(t.Context(), "1 ETH", "", "100 wei")) + + // Step 8: both balances visible, each in its own unit. + balAMock, err := driverA.GetPrivateBalance(t.Context(), "MOCK") + require.NoError(t, err) + require.NotNil(t, balAMock, "A should see a MOCK balance after deposit") + require.Equal(t, 0, depositMockRaw.Cmp(balAMock), + "A's MOCK balance should be 1000 MOCK (raw %s); got %s", depositMockRaw.String(), balAMock.String()) + + balAEth, err := driverA.GetPrivateBalance(t.Context(), "") + require.NoError(t, err) + require.NotNil(t, balAEth, "A should see an ETH balance after deposit") + require.Equal(t, 0, oneEther.Cmp(balAEth), + "A's ETH balance should be 1 ETH; got %s wei", balAEth.String()) + + // Step 9: private-transfer 300 MOCK to B. Only touches encrypted state. + require.NoError(t, driverA.PrivateTransfer(t.Context(), + driverB.UserAddress().Hex(), "300", "MOCK", "100 wei")) + + // Step 10: A's MOCK decreased; A's ETH MUST be unchanged. This is the + // multi-token isolation check — if getprivatebalance's tokenAddress + // filter is broken (or the wallet's event decrypt mis-routes), the ETH + // read would reflect the MOCK transfer (or return a stale/wrong value). + transferredMock := new(big.Int).Mul(big.NewInt(300), mockUnit) + remainingMock := new(big.Int).Sub(depositMockRaw, transferredMock) + + balAMock2, err := driverA.GetPrivateBalance(t.Context(), "MOCK") + require.NoError(t, err) + require.NotNil(t, balAMock2) + require.Equal(t, 0, remainingMock.Cmp(balAMock2), + "A's post-transfer MOCK balance should be 700 MOCK; got %s", balAMock2.String()) + + balAEth2, err := driverA.GetPrivateBalance(t.Context(), "") + require.NoError(t, err) + require.NotNil(t, balAEth2, "A's ETH balance should still be visible after MOCK transfer") + require.Equal(t, 0, oneEther.Cmp(balAEth2), + "A's ETH balance should be UNCHANGED at 1 ETH after a MOCK-only transfer; got %s wei", balAEth2.String()) + + // Step 11: B sees the 300 MOCK — proves ERC-20 event routing reaches + // the recipient correctly. + balBMock, err := driverB.GetPrivateBalance(t.Context(), "MOCK") + require.NoError(t, err) + require.NotNil(t, balBMock, "B should see a MOCK balance after receiving the transfer") + require.Equal(t, 0, transferredMock.Cmp(balBMock), + "B's MOCK balance should be 300 MOCK; got %s", balBMock.String()) + + // Step 12: B must NOT see any ETH — A holds ETH but never sent any to B. + // If cross-user filtering leaked A's ETH events into B's view, this nil + // check would fail. + balBEth, err := driverB.GetPrivateBalance(t.Context(), "") + require.NoError(t, err) + require.Nil(t, balBEth, "B should have no ETH private balance — A holds ETH but never transferred any to B") + + t.Log("ERC-20 private transfer with multi-token isolation verified: non-18 decimals, per-token filter, per-user routing") +} diff --git a/runtime/wasm-go/fullstack_tests/private_transfer_test.go b/runtime/wasm-go/fullstack_tests/private_transfer_test.go new file mode 100644 index 0000000..3d71f8e --- /dev/null +++ b/runtime/wasm-go/fullstack_tests/private_transfer_test.go @@ -0,0 +1,109 @@ +package main_test + +import ( + "math/big" + "os" + "path/filepath" + "testing" + + "github.com/HorizenOfficial/vela-nova/payment-app/testhelpers" + walletTestutil "github.com/HorizenOfficial/vela-nova/wallet/testutil" + "github.com/HorizenOfficial/vela/pkg/testutil/fullstack" + "github.com/stretchr/testify/require" +) + +// TestPrivateTransfer_TwoUsers exercises the payment-app's central feature — +// encrypted transfers between two registered users — end-to-end through the +// wallet CLI layer. Fullstack-unique value: validates that subgraph event +// indexing delivers *distinct* encrypted events to sender vs recipient, and +// that each party can only decrypt its own side (sender sees a +// "transfer_sent" event with their new balance; recipient sees a +// "transfer_received" event with their balance). +// +// Flow: +// 1. Two drivers = two funded users. Each driver has its own temp wallet.conf, +// P521 key, and blockchain client bound to its own secp key. +// 2. User A deploys the payment app. User B points its conf at the same +// ApplicationID (SetApplicationID). +// 3. Both register. A deposits 1 ETH. A private-transfers 0.3 ETH to B. +// 4. Assert A's private balance == 0.7 ETH (via A's driver, which only +// decrypts events addressed to A). +// 5. Assert B's private balance == 0.3 ETH (via B's driver, decrypting its +// own events). If event-subtype derivation is broken, one or both reads +// would return nil instead of the expected amount. +func TestPrivateTransfer_TwoUsers(t *testing.T) { + if os.Getenv("CI_FLAG") != "" { + t.Skip("Skipping fullstack test in CI environment") + } + + t.Setenv("MANAGER_ARTIFACTS_PATH", t.TempDir()) + + suite := fullstack.NewFullStackSystemTestSuite(t, "wasmtime-payment", + testhelpers.NewNetworkLogConfig(), testhelpers.NewNetworkLogConfig()) + defer suite.Cleanup() + + require.NoError(t, suite.StartExecutor()) + require.NoError(t, suite.StartManager()) + + // Two independent wallet drivers = two funded users, two P521 keys, + // two wallet.conf files. Each driver grants its user DEPLOYER_ROLE + // (harmless redundancy; only one will deploy). + driverA := walletTestutil.NewWalletDriver(t, suite) + driverB := walletTestutil.NewWalletDriver(t, suite) + + wasmBytes := testhelpers.BuildAndLoadWasmModule(t) + wasmPath := filepath.Join(t.TempDir(), "payment_app.wasm") + require.NoError(t, os.WriteFile(wasmPath, wasmBytes, 0o644)) + + // A deploys; B gets told about the resulting ApplicationID so its + // commands (RegisterUser, GetPrivateBalance) resolve to the same app. + appID, err := driverA.DeployApp(t.Context(), wasmPath, "100 wei", nil) + require.NoError(t, err) + require.NotZero(t, appID) + driverB.SetApplicationID(appID) + + // Both users associate their P521 keys with the app. Seed registration + // is what makes subtype-filtered event queries work — without it, each + // user would fail to fetch their own events. + require.NoError(t, driverA.RegisterUser(t.Context(), "100 wei")) + require.NoError(t, driverB.RegisterUser(t.Context(), "100 wei")) + + // A deposits 1 ETH. Verify A's private balance reflects the deposit. + require.NoError(t, driverA.Deposit(t.Context(), "1 ETH", "", "100 wei")) + + oneEther := new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil) + balA, err := driverA.GetPrivateBalance(t.Context(), "") + require.NoError(t, err) + require.NotNil(t, balA, "A should see a balance event after deposit") + require.Equal(t, 0, oneEther.Cmp(balA), + "A's post-deposit balance should be 1 ETH; got %s wei", balA.String()) + + // B should see no balance yet — nothing has arrived. + balB0, err := driverB.GetPrivateBalance(t.Context(), "") + require.NoError(t, err) + require.Nil(t, balB0, "B should have no private balance before the transfer") + + // A private-transfers 0.3 ETH to B. This generates two encrypted events + // on the same state update: one routed to A ("transfer_sent") with A's + // post-transfer balance, one to B ("transfer_received") with B's new + // balance. The subgraph indexes both. + require.NoError(t, driverA.PrivateTransfer(t.Context(), + driverB.UserAddress().Hex(), "0.3 ETH", "", "100 wei")) + + transferred := new(big.Int).Mul(big.NewInt(3), new(big.Int).Exp(big.NewInt(10), big.NewInt(17), nil)) // 0.3 ETH + remaining := new(big.Int).Sub(oneEther, transferred) // 0.7 ETH + + balA2, err := driverA.GetPrivateBalance(t.Context(), "") + require.NoError(t, err) + require.NotNil(t, balA2, "A should see a balance event after sending the transfer") + require.Equal(t, 0, remaining.Cmp(balA2), + "A's post-transfer balance should be 0.7 ETH; got %s wei", balA2.String()) + + balB, err := driverB.GetPrivateBalance(t.Context(), "") + require.NoError(t, err) + require.NotNil(t, balB, "B should see a balance event after receiving the transfer") + require.Equal(t, 0, transferred.Cmp(balB), + "B's post-transfer balance should be 0.3 ETH; got %s wei", balB.String()) + + t.Log("Private transfer between two users — balances, subgraph indexing, and per-user decryption all verified") +} From 9d6b502edc8b20f10343a3e014e3473c5c3e5f8b Mon Sep 17 00:00:00 2001 From: Alberto Sala Date: Thu, 23 Apr 2026 15:11:34 +0200 Subject: [PATCH 10/25] In getpublicbalance replaced live-RPC test with simulated-backend --- wallet/cmd/downloadreport.go | 112 ++++++++++--------- wallet/cmd/downloadreport_test.go | 3 +- wallet/cmd/getpublicbalance.go | 101 +++++++++++------ wallet/cmd/getpublicbalance_test.go | 70 +++++++++--- wallet/cmd/privatetransfer.go | 164 +++++++++++++--------------- wallet/cmd/requestreport.go | 127 ++++++++++----------- wallet/testutil/wallet_driver.go | 10 ++ wallet/testutil/wrappers.go | 67 ++++++++++++ 8 files changed, 394 insertions(+), 260 deletions(-) diff --git a/wallet/cmd/downloadreport.go b/wallet/cmd/downloadreport.go index 3f25a15..cc21f9c 100644 --- a/wallet/cmd/downloadreport.go +++ b/wallet/cmd/downloadreport.go @@ -9,10 +9,10 @@ import ( "path/filepath" "strings" - "github.com/ethereum/go-ethereum/ethclient" "github.com/HorizenOfficial/vela-nova/wallet/app" "github.com/HorizenOfficial/vela/pkg/blockchain" "github.com/HorizenOfficial/vela/pkg/common" + "github.com/ethereum/go-ethereum/ethclient" "github.com/spf13/cobra" ) @@ -36,31 +36,7 @@ func (c *DownloadReportCommand) Command() *cobra.Command { Short: "download a deanonymization report from the authority service", Long: "download a deanonymization report from the authority service, optionally decrypting it after download", Run: func(cmd *cobra.Command, args []string) { - if err := c.RequireApplicationID(); err != nil { - fmt.Printf("Error: %v\n", err) - return - } - ctx := context.Background() - if cmd != nil && cmd.Context() != nil { - ctx = cmd.Context() - } - if c.reportID == "" { - fmt.Println("Error: report id is required") - return - } - if c.Config.AuthorityServiceURL == "" { - fmt.Println("Error: AuthorityServiceURL is not configured") - return - } - if c.Config.KeySecp == nil { - fmt.Println("Error: secp256k1 key not configured") - return - } - if c.destPath == "" { - c.destPath = filepath.Join(".", c.reportID) - } - - if err := c.run(ctx); err != nil { + if err := c.Exec(resolveContext(cmd)); err != nil { fmt.Printf("Error: %v\n", err) } }, @@ -73,47 +49,81 @@ func (c *DownloadReportCommand) Command() *cobra.Command { return cmd } -func (c *DownloadReportCommand) run(ctx context.Context) error { - type decryptedReportOutput struct { - ApplicationID common.ApplicationIdType `json:"applicationId"` - RequestID common.RequestIdType `json:"requestId"` - ReportData json.RawMessage `json:"reportData,omitempty"` - ReportDataBase64 string `json:"reportDataBase64,omitempty"` - Authority string `json:"authority,omitempty"` +// Exec runs the downloadreport flow outside of the Cobra wrapper. Exported so +// test drivers can invoke it directly after setting flag-bound struct fields. +func (c *DownloadReportCommand) Exec(ctx context.Context) error { + if err := c.RequireApplicationID(); err != nil { + return err } - - if strings.TrimSpace(c.Config.RpcUrl) == "" { - return fmt.Errorf("rpcUrl not configured to auto-detect chain ID") + if c.reportID == "" { + return fmt.Errorf("report id is required") } - ethc, err := ethclient.DialContext(ctx, c.Config.RpcUrl) - if err != nil { - return fmt.Errorf("dialing rpc to fetch chain id: %w", err) + if c.Config.AuthorityServiceURL == "" { + return fmt.Errorf("AuthorityServiceURL is not configured") } - defer ethc.Close() - id, err := ethc.ChainID(ctx) - if err != nil { - return fmt.Errorf("fetching chain id: %w", err) + if c.Config.KeySecp == nil { + return fmt.Errorf("secp256k1 key not configured") + } + if c.destPath == "" { + c.destPath = filepath.Join(".", c.reportID) + } + + // Two paths for resolving chain ID: + // - In-process harness (fullstack tests): BlockchainClient is injected + // pre-connected to a simulated backend; we ask it directly. The conf's + // RpcUrl is a placeholder and cannot be dialed. + // - CLI / unit-tests: no BlockchainClient is injected; fall back to a + // lightweight ethclient.Dial on the configured RpcUrl just for chain + // id. This keeps CLI behavior unchanged. + var chainID uint64 + if c.BlockchainClient != nil { + chainIDBig, err := c.BlockchainClient.ChainID(ctx) + if err != nil { + return fmt.Errorf("fetching chain id from injected client: %w", err) + } + chainID = chainIDBig.Uint64() + } else { + if strings.TrimSpace(c.Config.RpcUrl) == "" { + return fmt.Errorf("rpcUrl not configured to auto-detect chain ID") + } + ethc, err := ethclient.DialContext(ctx, c.Config.RpcUrl) + if err != nil { + return fmt.Errorf("dialing rpc to fetch chain id: %w", err) + } + defer ethc.Close() + id, err := ethc.ChainID(ctx) + if err != nil { + return fmt.Errorf("fetching chain id: %w", err) + } + chainID = id.Uint64() } - chainID := id.Uint64() - client := app.NewAuthorityClient(c.Config.AuthorityServiceURL, chainID, c.Config.ApplicationID, c.Config.KeySecp) + authClient := app.NewAuthorityClient(c.Config.AuthorityServiceURL, chainID, c.Config.ApplicationID, c.Config.KeySecp) - nonceResp, err := client.FetchNonce(ctx) + nonceResp, err := authClient.FetchNonce(ctx) if err != nil { return err } - report, err := client.FetchReport(ctx, c.reportID, nonceResp) + report, err := authClient.FetchReport(ctx, c.reportID, nonceResp) if err != nil { return err } + type decryptedReportOutput struct { + ApplicationID common.ApplicationIdType `json:"applicationId"` + RequestID common.RequestIdType `json:"requestId"` + ReportData json.RawMessage `json:"reportData,omitempty"` + ReportDataBase64 string `json:"reportDataBase64,omitempty"` + Authority string `json:"authority,omitempty"` + } + var dataToWrite []byte if c.decrypt { - if c.BlockchainClient == nil { - if err := c.InitChainClient(ctx); err != nil { - return fmt.Errorf("connecting to rpc node: %w", err) - } + // Decrypt path needs a blockchain client to fetch the TEE public key. + // If not pre-injected, InitChainClient builds one from Config. + if err := c.InitChainClient(ctx); err != nil { + return fmt.Errorf("connecting to rpc node: %w", err) } defer c.CloseClient() diff --git a/wallet/cmd/downloadreport_test.go b/wallet/cmd/downloadreport_test.go index 9967cee..527c894 100644 --- a/wallet/cmd/downloadreport_test.go +++ b/wallet/cmd/downloadreport_test.go @@ -67,6 +67,7 @@ func TestDownloadReportWithoutDecryptSavesReport(t *testing.T) { KeySecp: keySecp, AuthorityServiceURL: ts.URL, RpcUrl: rpcSrv.URL, + ApplicationID: common.ApplicationIdType(1), } testutil.WriteTempConf(t, cfg) @@ -78,7 +79,7 @@ func TestDownloadReportWithoutDecryptSavesReport(t *testing.T) { cmd.destPath = destPath cmd.decrypt = false - err = cmd.run(context.Background()) + err = cmd.Exec(context.Background()) require.NoError(t, err) data, err := os.ReadFile(destPath) diff --git a/wallet/cmd/getpublicbalance.go b/wallet/cmd/getpublicbalance.go index 5a44081..ad0813e 100644 --- a/wallet/cmd/getpublicbalance.go +++ b/wallet/cmd/getpublicbalance.go @@ -3,18 +3,34 @@ package cmd import ( "context" "fmt" - "log" + "math/big" "github.com/HorizenOfficial/vela-nova/wallet/app" "github.com/spf13/cobra" + "github.com/ethereum/go-ethereum/accounts/abi/bind/v2" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" ) +// BalanceClient is the subset of a chain client needed to read public balances. +// BalanceAt serves the ETH path; bind.ContractBackend serves the ERC-20 path +// (for calling balanceOf via the generated contract bindings). Both +// *ethclient.Client (production) and simulated.Client (tests) satisfy it. +type BalanceClient interface { + bind.ContractBackend + BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) +} + type GetPublicBalanceCommand struct { *app.AppCommand token string + + // Client is an optional injected chain client. When nil (the normal CLI + // path) Exec dials Config.RpcUrl and closes the result. When set (tests, + // or callers that already hold a client) Exec uses it and leaves lifecycle + // management to the caller. + Client BalanceClient } func NewGetPublicBalanceCommand(config *app.Config) *GetPublicBalanceCommand { @@ -23,6 +39,55 @@ func NewGetPublicBalanceCommand(config *app.Config) *GetPublicBalanceCommand { } } +// Exec runs the public-balance lookup outside the Cobra wrapper. Exported so +// tests can invoke it directly after wiring Client / token. +func (c *GetPublicBalanceCommand) Exec(ctx context.Context) error { + if c.Config.KeySecp == nil { + return fmt.Errorf("Secp256k1 key not found in the wallet") + } + + tokenInfo, err := c.Config.Tokens.ResolveToken(c.token) + if err != nil { + return err + } + + if tokenInfo.Address != ETH_TOKEN { + // TODO: implement ERC-20 balanceOf via generated bindings using c.Client. + return fmt.Errorf("ERC-20 public balance query not yet implemented for %s", tokenInfo.Symbol) + } + + client, cleanup, err := c.resolveClient(ctx) + if err != nil { + return fmt.Errorf("connecting to rpc node: %w", err) + } + if cleanup != nil { + defer cleanup() + } + + account := common.HexToAddress(c.Config.KeySecp.PublicKey().Address()) + balance, err := client.BalanceAt(ctx, account, nil) + if err != nil { + return fmt.Errorf("failed to get balance: %w", err) + } + + fmt.Println(c.Config.Tokens.FormatAmount(balance, tokenInfo)) + return nil +} + +// resolveClient returns the injected client if one was set, otherwise dials +// Config.RpcUrl. The cleanup func is nil for the injected path (caller owns +// lifecycle) and closes the dialed client for the production path. +func (c *GetPublicBalanceCommand) resolveClient(ctx context.Context) (BalanceClient, func(), error) { + if c.Client != nil { + return c.Client, nil, nil + } + dialed, err := ethclient.DialContext(ctx, c.Config.RpcUrl) + if err != nil { + return nil, nil, err + } + return dialed, func() { dialed.Close() }, nil +} + func (c *GetPublicBalanceCommand) Command() *cobra.Command { cmd := &cobra.Command{ Use: "getpublicbalance", @@ -30,41 +95,9 @@ func (c *GetPublicBalanceCommand) Command() *cobra.Command { Long: `display public balance of this wallet (ETH or ERC-20 token)`, Run: func(cmd *cobra.Command, args []string) { - - if c.Config.KeySecp == nil { - fmt.Println("Error: Secp256k1 key not found in the wallet") - return - } - - // Resolve token - tokenInfo, err := c.Config.Tokens.ResolveToken(c.token) - if err != nil { + if err := c.Exec(resolveContext(cmd)); err != nil { fmt.Printf("Error: %v\n", err) - return } - - if tokenInfo.Address != ETH_TOKEN { - // TODO: implement ERC-20 balanceOf query - fmt.Printf("Error: ERC-20 public balance query not yet implemented for %s\n", tokenInfo.Symbol) - return - } - - address := c.Config.KeySecp.PublicKey().Address() - rpcURL := c.Config.RpcUrl - - client, err := ethclient.Dial(rpcURL) - if err != nil { - log.Fatalf("Failed to connect to RPC: %v", err) - } - defer client.Close() - - account := common.HexToAddress(address) - balance, err := client.BalanceAt(context.Background(), account, nil) - if err != nil { - log.Fatalf("Failed to get balance: %v", err) - } - - fmt.Println(c.Config.Tokens.FormatAmount(balance, tokenInfo)) }, } cmd.Flags().StringVarP(&c.token, "token", "k", "", "Token symbol or address (default: ETH)") diff --git a/wallet/cmd/getpublicbalance_test.go b/wallet/cmd/getpublicbalance_test.go index df6179d..aaf24ef 100644 --- a/wallet/cmd/getpublicbalance_test.go +++ b/wallet/cmd/getpublicbalance_test.go @@ -3,36 +3,74 @@ package cmd import ( "bytes" "io" + "math/big" "os" "testing" "github.com/HorizenOfficial/vela-nova/wallet/app" + pestestutil "github.com/HorizenOfficial/vela/pkg/blockchain/testutil" "github.com/HorizenOfficial/vela/pkg/crypto" + ethCommon "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestGetPublicBalanceCmd(t *testing.T) { - // Redirect stdout +// TestGetPublicBalanceCmd_ZeroBalance verifies the command prints "0 ETH" for +// a freshly-generated (unfunded) wallet key. Uses an injected simulated chain +// client — no live RPC dependency, unlike the previous version that dialled +// base-sepolia and failed on DNS flakes. +func TestGetPublicBalanceCmd_ZeroBalance(t *testing.T) { old := os.Stdout - r, w, _ := os.Pipe() + r, w, err := os.Pipe() + require.NoError(t, err) os.Stdout = w + defer func() { os.Stdout = old }() - var key, _ = crypto.GeneratePrivateKeySecp256k1() + testHelper := pestestutil.NewSimTestHelper(t, true, true, nil, nil) + defer testHelper.Close() - // Execute the command - cmd := NewGetPublicBalanceCommand(&app.Config{ - KeySecp: key, - RpcUrl: "https://base-sepolia.drpc.org", - }).Command() - cmd.Run(cmd, nil) + key, err := crypto.GeneratePrivateKeySecp256k1() + require.NoError(t, err) + + cmd := NewGetPublicBalanceCommand(&app.Config{KeySecp: key}) + cmd.Client = testHelper.Client() + require.NoError(t, cmd.Exec(t.Context())) - // Restore stdout w.Close() - os.Stdout = old + var buf bytes.Buffer + _, err = io.Copy(&buf, r) + require.NoError(t, err) + assert.Contains(t, buf.String(), "0 ETH") +} + +// TestGetPublicBalanceCmd_FundedBalance exercises the non-zero ETH formatting +// path by funding a fresh key on the simulated chain and then reading its +// public balance through the command. Complements the zero-balance test — +// together they cover both branches of the FormatAmount output. +func TestGetPublicBalanceCmd_FundedBalance(t *testing.T) { + old := os.Stdout + r, w, err := os.Pipe() + require.NoError(t, err) + os.Stdout = w + defer func() { os.Stdout = old }() + testHelper := pestestutil.NewSimTestHelper(t, true, true, nil, nil) + defer testHelper.Close() + + key, err := crypto.GeneratePrivateKeySecp256k1() + require.NoError(t, err) + + userAddr := ethCommon.HexToAddress(key.PublicKey().Address()) + fundAmount := new(big.Int).Mul(big.NewInt(2), new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil)) + testHelper.WaitMined(testHelper.TransferFunds(testHelper.Deployer, userAddr, fundAmount)) + + cmd := NewGetPublicBalanceCommand(&app.Config{KeySecp: key}) + cmd.Client = testHelper.Client() + require.NoError(t, cmd.Exec(t.Context())) + + w.Close() var buf bytes.Buffer - io.Copy(&buf, r) - output := buf.String() - - assert.Contains(t, output, "0 ETH") + _, err = io.Copy(&buf, r) + require.NoError(t, err) + assert.Contains(t, buf.String(), "2 ETH") } diff --git a/wallet/cmd/privatetransfer.go b/wallet/cmd/privatetransfer.go index 13f2feb..2f23e0e 100644 --- a/wallet/cmd/privatetransfer.go +++ b/wallet/cmd/privatetransfer.go @@ -3,7 +3,6 @@ package cmd import ( "context" "fmt" - "log" "math/big" "github.com/HorizenOfficial/vela-common-go/wasm/types" @@ -31,106 +30,91 @@ func NewPrivateTransferCommand(config *app.Config, blockchainClient blockchain.C return cmd } -func (c *PrivateTransferCommand) Command() *cobra.Command { - cmd := &cobra.Command{ - Use: "privatetransfer", - Short: `submits a private transfer request to a receiver address`, - Long: `submits a private transfer request to a receiver address`, - Run: func(cmd *cobra.Command, args []string) { - if err := c.RequireApplicationID(); err != nil { - log.Fatalf("Error: %v", err) - } +// Exec runs the privatetransfer flow outside of the Cobra wrapper. Exported +// so test drivers can invoke it directly after setting flag-bound struct +// fields. +func (c *PrivateTransferCommand) Exec(ctx context.Context) error { + if err := c.RequireApplicationID(); err != nil { + return err + } - // Resolve token - tokenInfo, err := c.Config.Tokens.ResolveToken(c.token) - if err != nil { - fmt.Printf("Error: %v\n", err) - return - } + tokenInfo, err := c.Config.Tokens.ResolveToken(c.token) + if err != nil { + return err + } - //get receiver - to, err := app.ValidateAndChecksumAddress(c.receiver) - if err != nil { - log.Fatalf("Error: invalid receiver: %s\n", c.receiver) - } + to, err := app.ValidateAndChecksumAddress(c.receiver) + if err != nil { + return fmt.Errorf("invalid receiver address: %w", err) + } - // Parse amount with correct decimals - var amount *big.Int - if tokenInfo.Address == ETH_TOKEN { - amount, err = app.ParseEtherValue(c.value) - } else { - amount, err = c.Config.Tokens.ParseAmount(c.value, tokenInfo) - } - if err != nil { - fmt.Printf("Error: invalid amount: %v\n", err) - return - } + amount, err := parseAssetAmount(c.Config.Tokens, tokenInfo, c.value) + if err != nil { + return fmt.Errorf("invalid amount: %w", err) + } - maxFeeValue, err := app.ParseEtherValue(c.maxFeeValue) - if err != nil { - fmt.Printf("Error: invalid max fee amount: %v\n", err) - return - } + maxFeeValue, err := app.ParseEtherValue(c.maxFeeValue) + if err != nil { + return fmt.Errorf("invalid max fee amount: %w", err) + } - ctx := context.Background() - if cmd != nil && cmd.Context() != nil { - ctx = cmd.Context() - } - if c.BlockchainClient == nil { - if err := c.InitChainClient(ctx); err != nil { - fmt.Printf("Error connecting to rpc node: %v\n", err) - return - } - } - defer c.CloseClient() + if err := c.InitChainClient(ctx); err != nil { + return fmt.Errorf("connecting to rpc node: %w", err) + } + defer c.CloseClient() - toAddr, err := types.HexToAddress(to.Hex()) - if err != nil { - log.Fatalf("Error: invalid receiver: %s\n", c.receiver) - } + toAddr, err := types.HexToAddress(to.Hex()) + if err != nil { + return fmt.Errorf("invalid receiver address, could not convert to byte array: %w", err) + } - if len(c.invoiceID) > runtimeapp.MaxInvoiceIDLength { - fmt.Printf("Error: invoice_id exceeds maximum length of %d characters\n", runtimeapp.MaxInvoiceIDLength) - return - } + if len(c.invoiceID) > runtimeapp.MaxInvoiceIDLength { + return fmt.Errorf("invoice_id exceeds maximum length of %d characters", runtimeapp.MaxInvoiceIDLength) + } - // Resolve token address for the encrypted payload - tokenAddr, err := types.HexToAddress(tokenInfo.Address.Hex()) - if err != nil { - log.Fatalf("Error: invalid token address: %v\n", err) - } + tokenAddr, err := types.HexToAddress(tokenInfo.Address.Hex()) + if err != nil { + return fmt.Errorf("invalid token address: %w", err) + } - //build payload with type transfer (token identity goes in the encrypted payload) - payload := runtimeapp.PayloadInstructions{ - Type: "transfer", - Transfer: &runtimeapp.TransferInstruction{ - To: toAddr, - TokenAddress: tokenAddr, - Amount: new(types.Uint256).SetBytes(amount.Bytes()), - InvoiceID: c.invoiceID, - }, - } - encryptedPayload, err := c.EncryptPayload(&payload, ctx) - if err != nil { - log.Fatalf("Error encrypting private transfer payload: %v", err) - } + payload := runtimeapp.PayloadInstructions{ + Type: "transfer", + Transfer: &runtimeapp.TransferInstruction{ + To: toAddr, + TokenAddress: tokenAddr, + Amount: new(types.Uint256).SetBytes(amount.Bytes()), + InvoiceID: c.invoiceID, + }, + } + encryptedPayload, err := c.EncryptPayload(&payload, ctx) + if err != nil { + return fmt.Errorf("encrypting private transfer payload: %w", err) + } - // On-chain: no business asset deposited (it's a private-state transfer), - // so tokenAddress=ETH_TOKEN and assetAmount=0. - requestType := common.Process - requestID, _, err := c.BlockchainClient.SubmitRequest(ctx, PROTOCOL_VERSION, c.Config.ApplicationID, requestType, encryptedPayload, ETH_TOKEN, big.NewInt(0), maxFeeValue) - if err != nil { - fmt.Printf("Error sending request to transfer amount %s to %s: %v", c.value, to, err) - return - } - fmt.Printf("Waiting for confirmation from Vela for requestID: %s\n", requestID) - err = c.WaitForRequestCompleted(requestID, ctx) - if err != nil { - fmt.Printf("Private transfer failed: %v\n", err) - return - } + // On-chain: no business asset deposited (it's a private-state transfer), + // so tokenAddress=ETH_TOKEN and assetAmount=0. + requestID, _, err := c.BlockchainClient.SubmitRequest(ctx, PROTOCOL_VERSION, c.Config.ApplicationID, common.Process, encryptedPayload, ETH_TOKEN, big.NewInt(0), maxFeeValue) + if err != nil { + return fmt.Errorf("sending request to transfer amount %s to %s: %w", c.value, to, err) + } - fmt.Println("Private transfer completed successfully") + fmt.Printf("Waiting for confirmation from Vela for requestID: %s\n", requestID) + if err := c.WaitForRequestCompleted(requestID, ctx); err != nil { + return fmt.Errorf("Private transfer failed: %w", err) + } + fmt.Println("Private transfer completed successfully") + return nil +} + +func (c *PrivateTransferCommand) Command() *cobra.Command { + cmd := &cobra.Command{ + Use: "privatetransfer", + Short: `submits a private transfer request to a receiver address`, + Long: `submits a private transfer request to a receiver address`, + Run: func(cmd *cobra.Command, args []string) { + if err := c.Exec(resolveContext(cmd)); err != nil { + fmt.Printf("Error: %v\n", err) + } }, } cmd.Flags().StringVarP(&c.value, "amount", "a", "", "The amount to transfer (e.g., '1.5 ETH', '100' for ERC-20)") diff --git a/wallet/cmd/requestreport.go b/wallet/cmd/requestreport.go index 679dee3..27f89d8 100644 --- a/wallet/cmd/requestreport.go +++ b/wallet/cmd/requestreport.go @@ -5,8 +5,8 @@ import ( "fmt" "math/big" - runtimeapp "github.com/HorizenOfficial/vela-nova/payment-app/app" "github.com/HorizenOfficial/vela-common-go/wasm/types" + runtimeapp "github.com/HorizenOfficial/vela-nova/payment-app/app" "github.com/HorizenOfficial/vela-nova/wallet/app" "github.com/HorizenOfficial/vela/pkg/blockchain" "github.com/HorizenOfficial/vela/pkg/common" @@ -28,81 +28,72 @@ type RequestReportCommand struct { toTimestamp int64 } -func (c *RequestReportCommand) Command() *cobra.Command { - cmd := &cobra.Command{ - Use: "requestreport", - Short: `requests a deanonymization report of the Nova app`, - Long: `requests a deanonymization report of the Nova app. Use --report-type to select 'balances' (default) or 'tx_history'.`, - Run: func(cmd *cobra.Command, args []string) { - if err := c.RequireApplicationID(); err != nil { - fmt.Printf("Error: %v\n", err) - return - } +// Exec runs the requestreport flow outside of the Cobra wrapper. Exported so +// test drivers can invoke it directly after setting flag-bound struct fields. +// Returns the assigned request ID so callers can pass it to /getreport. +func (c *RequestReportCommand) Exec(ctx context.Context) (common.RequestIdType, error) { + if err := c.RequireApplicationID(); err != nil { + return common.RequestIdType{}, err + } - maxFeeValue, err := app.ParseEtherValue(c.maxFeeValue) - if err != nil { - fmt.Printf("Error: invalid max fee amount: %v\n", err) - return - } + maxFeeValue, err := app.ParseEtherValue(c.maxFeeValue) + if err != nil { + return common.RequestIdType{}, fmt.Errorf("invalid max fee amount: %w", err) + } - ctx := context.Background() - if cmd != nil && cmd.Context() != nil { - ctx = cmd.Context() - } - if c.BlockchainClient == nil { - //create blockchain client - if err := c.InitChainClient(ctx); err != nil { - fmt.Printf("Error connecting to rpc node: %v\n", err) - return - } - } - defer c.CloseClient() + if err := c.InitChainClient(ctx); err != nil { + return common.RequestIdType{}, fmt.Errorf("connecting to rpc node: %w", err) + } + defer c.CloseClient() - if c.reportType != "" && c.reportType != "balances" && c.reportType != "tx_history" { - fmt.Printf("Error: unsupported report type %q (must be 'balances' or 'tx_history')\n", c.reportType) - return - } - if c.reportType == "tx_history" && c.address == "" { - fmt.Println("Error: --address is required for tx_history report type") - return - } + if c.reportType != "" && c.reportType != "balances" && c.reportType != "tx_history" { + return common.RequestIdType{}, fmt.Errorf("unsupported report type %q (must be 'balances' or 'tx_history')", c.reportType) + } + if c.reportType == "tx_history" && c.address == "" { + return common.RequestIdType{}, fmt.Errorf("--address is required for tx_history report type") + } - deanonInstruction := runtimeapp.DeanonymizeInstruction{ - ReportType: c.reportType, - FromTimestamp: c.fromTimestamp, - ToTimestamp: c.toTimestamp, - } - if c.address != "" { - addr, err := types.HexToAddress(c.address) - if err != nil { - fmt.Printf("Error: invalid address: %v\n", err) - return - } - deanonInstruction.Address = addr - } + deanonInstruction := runtimeapp.DeanonymizeInstruction{ + ReportType: c.reportType, + FromTimestamp: c.fromTimestamp, + ToTimestamp: c.toTimestamp, + } + if c.address != "" { + addr, err := types.HexToAddress(c.address) + if err != nil { + return common.RequestIdType{}, fmt.Errorf("invalid address: %w", err) + } + deanonInstruction.Address = addr + } - payload := runtimeapp.PayloadInstructions{Deanonymize: &deanonInstruction} - encryptedPayload, err := c.EncryptPayload(&payload, ctx) - if err != nil { - fmt.Printf("Error encrypting deanonymization payload: %v\n", err) - return - } + payload := runtimeapp.PayloadInstructions{Deanonymize: &deanonInstruction} + encryptedPayload, err := c.EncryptPayload(&payload, ctx) + if err != nil { + return common.RequestIdType{}, fmt.Errorf("encrypting deanonymization payload: %w", err) + } - requestType := common.Deanonymize - requestID, _, err := c.BlockchainClient.SubmitRequest(ctx, PROTOCOL_VERSION, c.Config.ApplicationID, requestType, encryptedPayload, ETH_TOKEN, big.NewInt(0), maxFeeValue) - if err != nil { - fmt.Printf("Error sending request to generate a deanonymization report: %v\n", err) - return - } + requestID, _, err := c.BlockchainClient.SubmitRequest(ctx, PROTOCOL_VERSION, c.Config.ApplicationID, common.Deanonymize, encryptedPayload, ETH_TOKEN, big.NewInt(0), maxFeeValue) + if err != nil { + return common.RequestIdType{}, fmt.Errorf("sending request to generate a deanonymization report: %w", err) + } - fmt.Println("Waiting for confirmation from Vela") - err = c.WaitForRequestCompleted(requestID, ctx) - if err != nil { - fmt.Printf("Deanonymization request failed: %v\n", err) - return - } - fmt.Printf("Deanonymization request completed successfully. Report id: %d_%s\n", c.Config.ApplicationID, requestID) + fmt.Println("Waiting for confirmation from Vela") + if err := c.WaitForRequestCompleted(requestID, ctx); err != nil { + return requestID, fmt.Errorf("Deanonymization request failed: %w", err) + } + fmt.Printf("Deanonymization request completed successfully. Report id: %d_%s\n", c.Config.ApplicationID, requestID) + return requestID, nil +} +func (c *RequestReportCommand) Command() *cobra.Command { + cmd := &cobra.Command{ + Use: "requestreport", + Short: `requests a deanonymization report of the Nova app`, + Long: `requests a deanonymization report of the Nova app. Use --report-type to select 'balances' (default) or 'tx_history'.`, + Run: func(cmd *cobra.Command, args []string) { + if _, err := c.Exec(resolveContext(cmd)); err != nil { + fmt.Printf("Error: %v\n", err) + } }, } cmd.Flags().StringVarP(&c.maxFeeValue, "max-value-fee", "f", "100 wei", "Maximum fee value reserved for this request (e.g., 0.1 ETH)") diff --git a/wallet/testutil/wallet_driver.go b/wallet/testutil/wallet_driver.go index 1366886..743cb43 100644 --- a/wallet/testutil/wallet_driver.go +++ b/wallet/testutil/wallet_driver.go @@ -182,6 +182,16 @@ func (d *WalletDriver) NewBlockchainClient() blockchain.Client { return d.newUserClient() } +// SetApplicationID writes the given application ID into the temp wallet.conf. +// Tests that want a second driver (e.g. recipient of a private transfer) to +// operate on an app deployed by a different driver call this after the first +// driver's DeployApp returns. Matches what the CLI would do: SaveApplicationID +// mutates the conf; subsequent loadConfig() picks it up. +func (d *WalletDriver) SetApplicationID(appID common.ApplicationIdType) { + d.t.Helper() + require.NoError(d.t, app.SaveApplicationID(d.confPath, appID)) +} + // AddToken appends an ERC-20 entry to the wallet.conf file so subsequent // wrapper calls can resolve the symbol (e.g. Deposit(..., "MOCK", ...)). The // driver does not mutate an in-memory registry — each wrapper re-loads the diff --git a/wallet/testutil/wrappers.go b/wallet/testutil/wrappers.go index cd9f274..88a36ad 100644 --- a/wallet/testutil/wrappers.go +++ b/wallet/testutil/wrappers.go @@ -121,6 +121,73 @@ func (d *WalletDriver) GetPrivateBalance(ctx context.Context, token string) (*bi return balance, nil } +// PrivateTransfer submits an encrypted transfer of `amount` of `token` from +// the wallet's user to `receiver` within the app's private state. Unlike +// Deposit/Withdraw this touches only the encrypted state — no on-chain asset +// movement — so both parties observe the result via decrypted events. +func (d *WalletDriver) PrivateTransfer(ctx context.Context, receiver, amount, token, maxFee string) error { + d.t.Helper() + + cfg := d.loadConfig() + c := cmd.NewPrivateTransferCommand(cfg, d.newUserClient()) + cobraCmd := c.Command() + require.NoError(d.t, cobraCmd.Flags().Set("to", receiver)) + require.NoError(d.t, cobraCmd.Flags().Set("amount", amount)) + require.NoError(d.t, cobraCmd.Flags().Set("max-value-fee", maxFee)) + if token != "" { + require.NoError(d.t, cobraCmd.Flags().Set("token", token)) + } + c.SubgraphClient = d.suite.GetSubgraph() + + if err := c.Exec(ctx); err != nil { + return fmt.Errorf("wallet driver: PrivateTransfer: %w", err) + } + return nil +} + +// RequestReport submits a Deanonymize request on-chain. The caller must have +// been granted the authority role for the app (e.g. via +// suite.RegisterAuthority) AND have registered as a user +// (driver.RegisterUser) so the executor can encrypt the resulting report to +// this wallet's P521 key. Returns the request ID, used as the reportID for +// the subsequent DownloadReport call. +func (d *WalletDriver) RequestReport(ctx context.Context, reportType, maxFee string) (common.RequestIdType, error) { + d.t.Helper() + + cfg := d.loadConfig() + c := cmd.NewRequestReportCommand(cfg, d.newUserClient()) + cobraCmd := c.Command() + require.NoError(d.t, cobraCmd.Flags().Set("max-value-fee", maxFee)) + if reportType != "" { + require.NoError(d.t, cobraCmd.Flags().Set("report-type", reportType)) + } + c.SubgraphClient = d.suite.GetSubgraph() + + reqID, err := c.Exec(ctx) + if err != nil { + return reqID, fmt.Errorf("wallet driver: RequestReport: %w", err) + } + return reqID, nil +} + +// DownloadReport runs the full /nonce + /getreport + decrypt flow and writes +// the result to destPath. Decrypt defaults to true so callers get the +// readable JSON form without exposing the wallet's P521 key to test code. +func (d *WalletDriver) DownloadReport(ctx context.Context, reportIDHex, destPath string) error { + d.t.Helper() + + cfg := d.loadConfig() + c := cmd.NewDownloadReportCommand(cfg, d.newUserClient()) + cobraCmd := c.Command() + require.NoError(d.t, cobraCmd.Flags().Set("report-id", reportIDHex)) + require.NoError(d.t, cobraCmd.Flags().Set("dest", destPath)) + + if err := c.Exec(ctx); err != nil { + return fmt.Errorf("wallet driver: DownloadReport: %w", err) + } + return nil +} + // ClaimPendingPayments pulls pending claims for `token` from the // ProcessorEndpoint contract into the user's public balance. func (d *WalletDriver) ClaimPendingPayments(ctx context.Context, token string) error { From 2ef2296289057ac541b493432b33f0fe3ac5f100 Mon Sep 17 00:00:00 2001 From: Alberto Sala Date: Thu, 23 Apr 2026 15:49:11 +0200 Subject: [PATCH 11/25] add TestMultiAppIsolation covering per-appID isolation --- .../multi_app_isolation_test.go | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 runtime/wasm-go/fullstack_tests/multi_app_isolation_test.go diff --git a/runtime/wasm-go/fullstack_tests/multi_app_isolation_test.go b/runtime/wasm-go/fullstack_tests/multi_app_isolation_test.go new file mode 100644 index 0000000..01a0697 --- /dev/null +++ b/runtime/wasm-go/fullstack_tests/multi_app_isolation_test.go @@ -0,0 +1,235 @@ +package main_test + +import ( + "math/big" + "os" + "path/filepath" + "testing" + + "github.com/HorizenOfficial/vela-nova/payment-app/testhelpers" + walletTestutil "github.com/HorizenOfficial/vela-nova/wallet/testutil" + "github.com/HorizenOfficial/vela/pkg/testutil/fullstack" + ethCommon "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +// TestMultiAppIsolation is the Tier 2 multi-tenancy test. It deploys two +// independent payment applications on the same simulated chain, registers +// the SAME user in both, and then verifies that every piece of state is +// scoped per-appID and cannot bleed across the boundary. A regression that +// shared state between apps would be catastrophic (cross-tenant data leak), +// so this invariant is worth a dedicated end-to-end test. +// +// ## What this uniquely catches +// +// Four layers of isolation are exercised in one flow: +// +// 1. **On-chain `appCustody[appID][token]`** — the ProcessorEndpoint +// contract keys deposited funds by (appID, token). A collapsed/shared +// map would let A's deposit show up in B's custody balance. +// +// 2. **Executor's encrypted state storage** — the versioned LevelDB layer +// stores each app's encrypted state under an appID-derived key. A bug +// that dropped/collapsed the appID portion would surface as the +// executor decrypting A's state when processing a request for B (or vice +// versa), corrupting both. +// +// 3. **Subgraph event filter** — `GetPrivateBalance` queries the subgraph +// with an `applicationID` filter. A missing/wrong filter would leak A's +// encrypted events into B's balance scan (the wallet would then try to +// decrypt them, typically succeed because the same user key is used in +// both apps, and report a wrong balance). +// +// 4. **WASM runtime instance separation** — the executor runs each app as +// its own Wasmtime module instance; runtime-level leakage (e.g. a shared +// module cache that accidentally shared memory) would also show up here. +// +// ## Design choices +// +// - **One user across both apps.** This is the tighter invariant: if +// events or storage were mis-keyed by user address (but happened to +// stay correctly keyed by appID), a two-distinct-users test would +// silently pass. With a single shared address, isolation MUST hold +// strictly by appID — there is no secondary key that could save us. +// +// - **ETH only.** ERC-20 plumbing (approve, mint, decimals) is orthogonal +// and already covered by `TestERC20FullStack` and the ERC-20 private +// transfer test. Keeping this ETH-only means any assertion failure +// points directly at an isolation bug, not a token-path issue. +// +// - **Same WASM bytes for both apps.** Apps differ on-chain by appID +// alone, so reusing the artifact makes the test strictly about the +// harness/executor/subgraph plumbing rather than per-app application +// logic. +// +// ## Flow (8 steps) +// +// 1. Start suite, executor, manager. +// 2. Build one wallet driver (single user, single key). +// 3. Deploy app A; same driver registers the user in A. +// 4. Deploy app B; same driver registers the user in B. DeployApp +// overwrites the driver's current ApplicationID, so after this the +// driver "points at" B. +// 5. Sanity pre-check — both custodies zero, both private balances nil. +// 6. Deposit 1 ETH into A. Assert A's custody = 1 ETH AND B's custody +// still 0. Assert A's private balance = 1 ETH AND B's private balance +// still nil. Any of these failing = a real isolation bug. +// 7. Deposit 0.5 ETH into B. Assert B's custody = 0.5 ETH AND A's custody +// still 1 ETH (unchanged by the B-side deposit). Assert B's private +// balance = 0.5 ETH AND A's private balance still 1 ETH. +// 8. Withdraw 0.3 ETH from A only. Assert A's custody = 0.7 ETH AND B's +// custody still 0.5 ETH (state-mutation on one app must not touch the +// other). Assert A's private balance = 0.7 ETH AND B's private balance +// still 0.5 ETH. +func TestMultiAppIsolation(t *testing.T) { + if os.Getenv("CI_FLAG") != "" { + t.Skip("Skipping fullstack test in CI environment") + } + + t.Setenv("MANAGER_ARTIFACTS_PATH", t.TempDir()) + + suite := fullstack.NewFullStackSystemTestSuite(t, "wasmtime-payment", + testhelpers.NewNetworkLogConfig(), testhelpers.NewNetworkLogConfig()) + defer suite.Cleanup() + + // Step 1: executor before manager — manager handshakes with executor at startup. + require.NoError(t, suite.StartExecutor()) + require.NoError(t, suite.StartManager()) + + // Step 2: a single wallet driver. One user, one secp256k1 key, one P521 + // key. The SAME key-pair is used against both apps below — that is the + // whole point of this test. + driver := walletTestutil.NewWalletDriver(t, suite) + + // Shared WASM bytes: both apps run the same payment-app. Any isolation + // we observe in the assertions below is attributable strictly to the + // harness plumbing, not to application-level code differences. + wasmBytes := testhelpers.BuildAndLoadWasmModule(t) + wasmPath := filepath.Join(t.TempDir(), "payment_app.wasm") + require.NoError(t, os.WriteFile(wasmPath, wasmBytes, 0o644)) + + // Step 3: deploy A and register user in A. + // DeployApp assigns a fresh ApplicationID server-side and persists it + // into the driver's temp wallet.conf. The follow-up RegisterUser call + // therefore targets app A automatically. + appAID, err := driver.DeployApp(t.Context(), wasmPath, "100 wei", nil) + require.NoError(t, err) + require.NotZero(t, appAID) + require.NoError(t, driver.RegisterUser(t.Context(), "100 wei")) + + // Step 4: deploy B and register the same user in B. + // DeployApp overwrites the driver's ApplicationID with appBID, so the + // subsequent RegisterUser scopes to B. After this block the driver + // "points at" B — we hop back to A explicitly via SetApplicationID + // whenever we need to. + appBID, err := driver.DeployApp(t.Context(), wasmPath, "100 wei", nil) + require.NoError(t, err) + require.NotZero(t, appBID) + require.NotEqual(t, appAID, appBID, "the two apps must receive distinct IDs (contract allocates sequentially)") + require.NoError(t, driver.RegisterUser(t.Context(), "100 wei")) + + // Handy constants reused across the assertions. + ethAddr := ethCommon.Address{} // sentinel for native ETH in appCustody + zero := big.NewInt(0) + oneEther := new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil) + halfEther := new(big.Int).Div(oneEther, big.NewInt(2)) + threeTenthsEther := new(big.Int).Mul(big.NewInt(3), new(big.Int).Exp(big.NewInt(10), big.NewInt(17), nil)) // 0.3 ETH + sevenTenthsEther := new(big.Int).Sub(oneEther, threeTenthsEther) // 0.7 ETH + + // Step 5: sanity pre-check — both apps MUST start empty. If either + // starts non-zero, something funky happened during Deploy/Register (e.g. + // crossed state from the other app) and the rest of the test would be + // meaningless. + require.Equal(t, 0, zero.Cmp(suite.GetAppCustody(appAID, ethAddr)), + "A's custody should start at 0") + require.Equal(t, 0, zero.Cmp(suite.GetAppCustody(appBID, ethAddr)), + "B's custody should start at 0") + + // Private balance is queried by flipping the driver's ApplicationID; the + // wrapper then scans the subgraph scoped to the current app. + driver.SetApplicationID(appAID) + balAInit, err := driver.GetPrivateBalance(t.Context(), "") + require.NoError(t, err) + require.Nil(t, balAInit, "A's private balance should be nil before any deposit") + + driver.SetApplicationID(appBID) + balBInit, err := driver.GetPrivateBalance(t.Context(), "") + require.NoError(t, err) + require.Nil(t, balBInit, "B's private balance should be nil before any deposit") + + // Step 6: deposit 1 ETH into A (hop driver back to A first). + // Two critical cross-app checks after this: + // - on-chain isolation: B's appCustody must remain 0 — if appCustody + // were collapsed to a single map, A's deposit would show in B. + // - subgraph/decrypt isolation: querying B's private balance must still + // return nil — if the subgraph's applicationID filter or the wallet's + // decrypt pipeline ignored appID, B would "see" A's deposit event. + driver.SetApplicationID(appAID) + require.NoError(t, driver.Deposit(t.Context(), "1 ETH", "", "100 wei")) + + require.Equal(t, 0, oneEther.Cmp(suite.GetAppCustody(appAID, ethAddr)), + "A's custody should be 1 ETH after A's deposit") + require.Equal(t, 0, zero.Cmp(suite.GetAppCustody(appBID, ethAddr)), + "B's custody must remain 0 — A's deposit leaked into B's appCustody") + + balA1, err := driver.GetPrivateBalance(t.Context(), "") + require.NoError(t, err) + require.NotNil(t, balA1, "A's private balance should be visible after A's deposit") + require.Equal(t, 0, oneEther.Cmp(balA1), "A's private balance should be 1 ETH") + + driver.SetApplicationID(appBID) + balB1, err := driver.GetPrivateBalance(t.Context(), "") + require.NoError(t, err) + require.Nil(t, balB1, "B's private balance must remain nil — A's deposit leaked into B's events") + + // Step 7: deposit 0.5 ETH into B (driver already points at B). + // Mirror checks: A's state must be UNCHANGED by B's deposit. This + // catches any bug where B's deposit transaction (appCustody write, event + // emission, executor state update) wasn't strictly scoped to appBID. + require.NoError(t, driver.Deposit(t.Context(), "0.5 ETH", "", "100 wei")) + + require.Equal(t, 0, halfEther.Cmp(suite.GetAppCustody(appBID, ethAddr)), + "B's custody should be 0.5 ETH after B's deposit") + require.Equal(t, 0, oneEther.Cmp(suite.GetAppCustody(appAID, ethAddr)), + "A's custody must remain 1 ETH — B's deposit perturbed A's appCustody") + + balB2, err := driver.GetPrivateBalance(t.Context(), "") + require.NoError(t, err) + require.NotNil(t, balB2, "B's private balance should be visible after B's deposit") + require.Equal(t, 0, halfEther.Cmp(balB2), "B's private balance should be 0.5 ETH") + + driver.SetApplicationID(appAID) + balA2, err := driver.GetPrivateBalance(t.Context(), "") + require.NoError(t, err) + require.NotNil(t, balA2, "A's private balance should still be visible") + require.Equal(t, 0, oneEther.Cmp(balA2), + "A's private balance must remain 1 ETH — B's deposit leaked into A's view") + + // Step 8: withdraw 0.3 ETH from A only. + // Deposits prove that writes don't leak. Withdrawals additionally prove + // that READS-then-WRITES scope correctly: a withdraw reads A's balance, + // debits it, and writes back. A bug that scoped the read or write to + // "latest app" rather than appID would drain B's custody here. + userHex := driver.UserAddress().Hex() + require.NoError(t, driver.Withdraw(t.Context(), "0.3 ETH", userHex, "", "100 wei")) + + require.Equal(t, 0, sevenTenthsEther.Cmp(suite.GetAppCustody(appAID, ethAddr)), + "A's custody should be 0.7 ETH after 0.3 ETH withdraw") + require.Equal(t, 0, halfEther.Cmp(suite.GetAppCustody(appBID, ethAddr)), + "B's custody must remain 0.5 ETH — A's withdraw touched B's custody") + + balA3, err := driver.GetPrivateBalance(t.Context(), "") + require.NoError(t, err) + require.NotNil(t, balA3) + require.Equal(t, 0, sevenTenthsEther.Cmp(balA3), + "A's private balance should be 0.7 ETH after the withdraw") + + driver.SetApplicationID(appBID) + balB3, err := driver.GetPrivateBalance(t.Context(), "") + require.NoError(t, err) + require.NotNil(t, balB3) + require.Equal(t, 0, halfEther.Cmp(balB3), + "B's private balance must remain 0.5 ETH — A's withdraw perturbed B's view") + + t.Log("Multi-app isolation verified: on-chain custody, subgraph event filter, executor state, and private-balance decrypt all scope correctly by appID") +} From 82114c47d1a6a10b5652bd9c89edda1842a95f4f Mon Sep 17 00:00:00 2001 From: Alberto Sala Date: Thu, 23 Apr 2026 16:02:34 +0200 Subject: [PATCH 12/25] Added ERC20 balanceOf and test in wallet cmd --- wallet/cmd/getpublicbalance.go | 68 ++++++++++++++++++++++++----- wallet/cmd/getpublicbalance_test.go | 63 ++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 10 deletions(-) diff --git a/wallet/cmd/getpublicbalance.go b/wallet/cmd/getpublicbalance.go index ad0813e..46af9ce 100644 --- a/wallet/cmd/getpublicbalance.go +++ b/wallet/cmd/getpublicbalance.go @@ -4,24 +4,41 @@ import ( "context" "fmt" "math/big" + "strings" "github.com/HorizenOfficial/vela-nova/wallet/app" "github.com/spf13/cobra" + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind/v2" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" ) // BalanceClient is the subset of a chain client needed to read public balances. -// BalanceAt serves the ETH path; bind.ContractBackend serves the ERC-20 path -// (for calling balanceOf via the generated contract bindings). Both +// BalanceAt serves the ETH path; bind.ContractBackend (specifically its +// ContractCaller.CallContract) serves the ERC-20 balanceOf path. Both // *ethclient.Client (production) and simulated.Client (tests) satisfy it. type BalanceClient interface { bind.ContractBackend BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) } +// erc20BalanceOfABI is the minimal ABI fragment needed to call balanceOf on +// any ERC-20. Parsed once at init — the signature is universal across tokens, +// so we do not pull in a specific token binding (which would pin us to a +// single implementation). +const erc20BalanceOfABIJSON = `[{"inputs":[{"name":"account","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"stateMutability":"view","type":"function"}]` + +var erc20BalanceOfABI = func() abi.ABI { + parsed, err := abi.JSON(strings.NewReader(erc20BalanceOfABIJSON)) + if err != nil { + panic(fmt.Sprintf("getpublicbalance: failed to parse erc20 balanceOf ABI: %v", err)) + } + return parsed +}() + type GetPublicBalanceCommand struct { *app.AppCommand token string @@ -51,11 +68,6 @@ func (c *GetPublicBalanceCommand) Exec(ctx context.Context) error { return err } - if tokenInfo.Address != ETH_TOKEN { - // TODO: implement ERC-20 balanceOf via generated bindings using c.Client. - return fmt.Errorf("ERC-20 public balance query not yet implemented for %s", tokenInfo.Symbol) - } - client, cleanup, err := c.resolveClient(ctx) if err != nil { return fmt.Errorf("connecting to rpc node: %w", err) @@ -65,15 +77,51 @@ func (c *GetPublicBalanceCommand) Exec(ctx context.Context) error { } account := common.HexToAddress(c.Config.KeySecp.PublicKey().Address()) - balance, err := client.BalanceAt(ctx, account, nil) - if err != nil { - return fmt.Errorf("failed to get balance: %w", err) + + var balance *big.Int + if tokenInfo.Address == ETH_TOKEN { + balance, err = client.BalanceAt(ctx, account, nil) + if err != nil { + return fmt.Errorf("failed to get ETH balance: %w", err) + } + } else { + balance, err = erc20BalanceOf(ctx, client, tokenInfo.Address, account) + if err != nil { + return fmt.Errorf("failed to get %s balance: %w", tokenInfo.Symbol, err) + } } fmt.Println(c.Config.Tokens.FormatAmount(balance, tokenInfo)) return nil } +// erc20BalanceOf performs a read-only balanceOf(account) call on `token` via +// an eth_call. Decodes the single uint256 return value. Relies on the ERC-20 +// balanceOf signature being universal across implementations, so no +// token-specific binding is required. +func erc20BalanceOf(ctx context.Context, client bind.ContractCaller, token, account common.Address) (*big.Int, error) { + calldata, err := erc20BalanceOfABI.Pack("balanceOf", account) + if err != nil { + return nil, fmt.Errorf("pack balanceOf: %w", err) + } + result, err := client.CallContract(ctx, ethereum.CallMsg{To: &token, Data: calldata}, nil) + if err != nil { + return nil, fmt.Errorf("call balanceOf: %w", err) + } + values, err := erc20BalanceOfABI.Unpack("balanceOf", result) + if err != nil { + return nil, fmt.Errorf("unpack balanceOf: %w", err) + } + if len(values) != 1 { + return nil, fmt.Errorf("balanceOf returned %d values, expected 1", len(values)) + } + balance, ok := values[0].(*big.Int) + if !ok { + return nil, fmt.Errorf("balanceOf returned unexpected type %T, expected *big.Int", values[0]) + } + return balance, nil +} + // resolveClient returns the injected client if one was set, otherwise dials // Config.RpcUrl. The cleanup func is nil for the injected path (caller owns // lifecycle) and closes the dialed client for the production path. diff --git a/wallet/cmd/getpublicbalance_test.go b/wallet/cmd/getpublicbalance_test.go index aaf24ef..a593398 100644 --- a/wallet/cmd/getpublicbalance_test.go +++ b/wallet/cmd/getpublicbalance_test.go @@ -11,6 +11,7 @@ import ( pestestutil "github.com/HorizenOfficial/vela/pkg/blockchain/testutil" "github.com/HorizenOfficial/vela/pkg/crypto" ethCommon "github.com/ethereum/go-ethereum/common" + "github.com/magiconair/properties" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -74,3 +75,65 @@ func TestGetPublicBalanceCmd_FundedBalance(t *testing.T) { require.NoError(t, err) assert.Contains(t, buf.String(), "2 ETH") } + +// TestGetPublicBalanceCmd_ERC20_Balance exercises the ERC-20 balanceOf path. +// Deploys MockERC20 with 6 decimals (USDC-like) on the simulated chain, checks +// the fresh user starts with a zero balance, then mints 1000 MOCK and confirms +// the command's output reflects the new balance with the correct decimals. The +// non-18 decimals keep the test honest — a hardcoded 10^18 divisor in the +// format path would surface here as the wrong number of whole tokens. +func TestGetPublicBalanceCmd_ERC20_Balance(t *testing.T) { + testHelper := pestestutil.NewSimTestHelper(t, true, true, nil, nil) + defer testHelper.Close() + + const mockDecimals = 6 + mockAddr := testHelper.DeployMockERC20("Mock Token", "MOCK", mockDecimals) + + key, err := crypto.GeneratePrivateKeySecp256k1() + require.NoError(t, err) + userAddr := ethCommon.HexToAddress(key.PublicKey().Address()) + + // Build a TokenRegistry that knows about MOCK. LoadTokenRegistry is the + // public API — it consumes a properties map, the same format wallet.conf + // uses, so the test exercises the same code path the CLI uses. + props := properties.NewProperties() + _, _, err = props.Set("token.MOCK.address", mockAddr.Hex()) + require.NoError(t, err) + _, _, err = props.Set("token.MOCK.decimals", "6") + require.NoError(t, err) + registry, err := app.LoadTokenRegistry(props) + require.NoError(t, err) + + runCmd := func() string { + old := os.Stdout + r, w, err := os.Pipe() + require.NoError(t, err) + os.Stdout = w + defer func() { os.Stdout = old }() + + cmd := NewGetPublicBalanceCommand(&app.Config{KeySecp: key, Tokens: registry}) + cmd.Client = testHelper.Client() + cobraCmd := cmd.Command() + require.NoError(t, cobraCmd.Flags().Set("token", "MOCK")) + require.NoError(t, cmd.Exec(t.Context())) + + w.Close() + var buf bytes.Buffer + _, err = io.Copy(&buf, r) + require.NoError(t, err) + return buf.String() + } + + // Before mint: zero MOCK. Confirms the ERC-20 path works for a fresh + // (never-minted-to) address and exercises the FormatAmount zero branch + // for a non-18-decimal token. + assert.Contains(t, runCmd(), "0 MOCK") + + // Mint 1000 MOCK (raw = 1000 * 10^6). The decimal count is the point — + // a hardcoded 10^18 divisor would print "0.000000000001 MOCK" instead. + mockUnit := new(big.Int).Exp(big.NewInt(10), big.NewInt(mockDecimals), nil) + mintAmount := new(big.Int).Mul(big.NewInt(1000), mockUnit) + testHelper.WaitMined(testHelper.MintERC20(userAddr, mintAmount)) + + assert.Contains(t, runCmd(), "1000 MOCK") +} From c0f9a257ef498fe5b3546df31e2a9b15181b1740 Mon Sep 17 00:00:00 2001 From: Alberto Sala Date: Thu, 23 Apr 2026 16:08:23 +0200 Subject: [PATCH 13/25] Added a test for authorizing a new deployer --- wallet/testutil/wallet_driver.go | 37 ++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/wallet/testutil/wallet_driver.go b/wallet/testutil/wallet_driver.go index 743cb43..cb19414 100644 --- a/wallet/testutil/wallet_driver.go +++ b/wallet/testutil/wallet_driver.go @@ -72,6 +72,23 @@ func NewWalletDriver(t *testing.T, suite *fullstack.FullStackSystemTestSuite) *W return NewWalletDriverWithKeys(t, suite, secpKey, p521Key) } +// NewWalletDriverNoDeployerRole is like NewWalletDriver but SKIPS the +// DEPLOYER_ROLE grant. Use it for tests that want to assert the contract-side +// role check — a driver built via this constructor will have its DeployApp +// calls reverted with DeployerNotAllowed until the caller subsequently calls +// suite.GrantDeployerRole on the driver's address. +func NewWalletDriverNoDeployerRole(t *testing.T, suite *fullstack.FullStackSystemTestSuite) *WalletDriver { + t.Helper() + + _, secpKey, err := suite.CreateFundedAccount() + require.NoError(t, err) + + p521Key, err := crypto.GeneratePrivateKeyP521() + require.NoError(t, err) + + return newWalletDriverInternal(t, suite, secpKey, p521Key, false) +} + // NewWalletDriverWithKeys is like NewWalletDriver but uses caller-supplied // keys. The user's secp key is registered with the suite (funded with ETH, // TransactOpts stored) and granted DEPLOYER_ROLE on the ProcessorEndpoint. @@ -82,6 +99,20 @@ func NewWalletDriverWithKeys( p521Key *cryptotypes.PrivateKeyP521, ) *WalletDriver { t.Helper() + return newWalletDriverInternal(t, suite, secpKey, p521Key, true) +} + +// newWalletDriverInternal is the shared body for the public constructors. +// grantDeployerRole gates the role grant so test variants can build a driver +// whose deploys will be rejected on-chain. +func newWalletDriverInternal( + t *testing.T, + suite *fullstack.FullStackSystemTestSuite, + secpKey *cryptotypes.PrivateKeySecp256k1, + p521Key *cryptotypes.PrivateKeyP521, + grantDeployerRole bool, +) *WalletDriver { + t.Helper() // Ensure the user account is funded and has a TransactOpts registered with // the suite. Skip if already registered (e.g. via NewWalletDriver → @@ -92,8 +123,10 @@ func NewWalletDriverWithKeys( suite.RegisterAccount(secpKey.PrivateKey) } - // Grant DEPLOYER_ROLE so wallet-driven DeployApp can submit deploy requests. - suite.GrantDeployerRole(userAddr) + if grantDeployerRole { + // Grant DEPLOYER_ROLE so wallet-driven DeployApp can submit deploy requests. + suite.GrantDeployerRole(userAddr) + } sim := suite.GetSimTestHelper() processor := sim.ProcessorContractAddress From 0f61f6712b443acc5739bb1f2dece5f66007f947 Mon Sep 17 00:00:00 2001 From: Alberto Sala Date: Thu, 23 Apr 2026 18:19:27 +0200 Subject: [PATCH 14/25] add TestDeployerRoleEnforcement verifying DEPLOYER_ROLE contract-side check --- .../fullstack_tests/deployer_role_test.go | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 runtime/wasm-go/fullstack_tests/deployer_role_test.go diff --git a/runtime/wasm-go/fullstack_tests/deployer_role_test.go b/runtime/wasm-go/fullstack_tests/deployer_role_test.go new file mode 100644 index 0000000..ca8f31b --- /dev/null +++ b/runtime/wasm-go/fullstack_tests/deployer_role_test.go @@ -0,0 +1,86 @@ +package main_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/HorizenOfficial/vela-nova/payment-app/testhelpers" + walletTestutil "github.com/HorizenOfficial/vela-nova/wallet/testutil" + "github.com/HorizenOfficial/vela/pkg/testutil/fullstack" + "github.com/stretchr/testify/require" +) + +// TestDeployerRoleEnforcement asserts both directions of the ProcessorEndpoint +// DEPLOYER_ROLE access-control invariant: +// +// 1. A user WITHOUT DEPLOYER_ROLE cannot deploy — the contract reverts with +// DeployerNotAllowed (see ProcessorEndpoint.sol's deployRequest guard). +// 2. After the admin grants DEPLOYER_ROLE to that same user, the EXACT SAME +// deploy call succeeds. +// +// This is the one invariant the existing fullstack suite does not explicitly +// verify. Every current wallet-driven deploy test uses NewWalletDriver, which +// grants DEPLOYER_ROLE unconditionally during construction — so the tests +// exercise the role-granted path but never the rejection path. If the +// contract-side check was accidentally removed or short-circuited to true, +// all 7 current fullstack tests would still pass. This test catches that +// regression. +// +// The test is fullstack-unique: mocks stub the contract, so they cannot +// assert that the role gate is actually present and enforced. +// +// Flow: +// 1. Start suite, executor, manager. +// 2. Build driver via NewWalletDriverNoDeployerRole — creates a funded user +// WITHOUT the role grant (single-purpose constructor; NewWalletDriver's +// role-granting behavior is unchanged). +// 3. Attempt DeployApp → expect error containing "DeployerNotAllowed". +// 4. suite.GrantDeployerRole(driver.UserAddress()) — admin grants the role. +// 5. Retry DeployApp → expect success, non-zero appID. +func TestDeployerRoleEnforcement(t *testing.T) { + if os.Getenv("CI_FLAG") != "" { + t.Skip("Skipping fullstack test in CI environment") + } + + t.Setenv("MANAGER_ARTIFACTS_PATH", t.TempDir()) + + suite := fullstack.NewFullStackSystemTestSuite(t, "wasmtime-payment", + testhelpers.NewNetworkLogConfig(), testhelpers.NewNetworkLogConfig()) + defer suite.Cleanup() + + require.NoError(t, suite.StartExecutor()) + require.NoError(t, suite.StartManager()) + + // Build a driver that is funded + registered, but has NOT been granted + // DEPLOYER_ROLE. Parallel constructor — existing NewWalletDriver (used by + // every other test) still grants the role; this variant exists solely for + // access-control tests like this one. + driver := walletTestutil.NewWalletDriverNoDeployerRole(t, suite) + + wasmBytes := testhelpers.BuildAndLoadWasmModule(t) + wasmPath := filepath.Join(t.TempDir(), "payment_app.wasm") + require.NoError(t, os.WriteFile(wasmPath, wasmBytes, 0o644)) + + // Step 3: deploy without the role — must be rejected on-chain. + // The simulated backend surfaces the revert reason through the tx + // submission error, wrapped by the wallet's DeployApp wrapper. + _, err := driver.DeployApp(t.Context(), wasmPath, "100 wei", nil) + require.Error(t, err, "DeployApp must fail when user lacks DEPLOYER_ROLE") + require.Contains(t, err.Error(), "DeployerNotAllowed", + "expected DeployerNotAllowed revert reason in error; got: %v", err) + + // Step 4: admin grants DEPLOYER_ROLE to this user. + // GrantDeployerRole is a simHelper call — runs as the constructor-deployer + // (admin) which holds ADMIN role, the only role that can grant DEPLOYER_ROLE. + suite.GrantDeployerRole(driver.UserAddress()) + + // Step 5: retry — exact same call, same user, same payload. Only the + // role state changed. Success here proves the grant is what unblocks + // deploy (not some other incidental state). + appID, err := driver.DeployApp(t.Context(), wasmPath, "100 wei", nil) + require.NoError(t, err, "DeployApp must succeed after DEPLOYER_ROLE is granted") + require.NotZero(t, appID, "successful deploy must assign a non-zero ApplicationID") + + t.Logf("Deploy rejected without DEPLOYER_ROLE; same user succeeded (app %d) after grant", appID) +} From 299cea0b889a7ced92c70f4f38804b70effb2109 Mon Sep 17 00:00:00 2001 From: Alberto Sala Date: Fri, 24 Apr 2026 11:25:36 +0200 Subject: [PATCH 15/25] Updated code comments with most recent changes --- .../fullstack_tests/deanonymize_test.go | 22 ++++++++------- .../fullstack_tests/erc20_fullstack_test.go | 16 +++++------ .../multi_app_isolation_test.go | 2 +- .../system_tests/payment_app_system_test.go | 27 +++++++++++-------- wallet/testutil/wallet_driver.go | 6 +++-- wallet/testutil/wallet_driver_test.go | 13 ++++----- wallet/testutil/wrappers.go | 3 ++- 7 files changed, 50 insertions(+), 39 deletions(-) diff --git a/runtime/wasm-go/fullstack_tests/deanonymize_test.go b/runtime/wasm-go/fullstack_tests/deanonymize_test.go index 1548433..9e095a2 100644 --- a/runtime/wasm-go/fullstack_tests/deanonymize_test.go +++ b/runtime/wasm-go/fullstack_tests/deanonymize_test.go @@ -17,9 +17,9 @@ import ( // TestDeanonymize_RealRoundTrip exercises the full authority deanonymization // pipeline against real components — no synthetic injection. It is the -// end-to-end counterpart to Phase 3's /nonce smoke test, which only -// verified the HTTP service boot and signature check against a hand-written -// report file. +// end-to-end counterpart to TestAuthorityServiceGetReportRoundTrip (in the +// vela fullstack package), which only verifies the HTTP service boot and +// signature check against a hand-written report file. // // Flow: // 1. Two wallet drivers: user (deposits ETH so there's something to report) @@ -81,9 +81,10 @@ func TestDeanonymize_RealRoundTrip(t *testing.T) { // application — otherwise the Deanonymize submitRequest reverts with // AuthorityNotAllowed. suite.RegisterAuthority(appID) - // suite.RegisterAuthority grants the *in-process authority* (Phase 3 - // key) the default-authority role, but our driverAuth is a different - // address. Grant it too via the lower-level helper. + // suite.RegisterAuthority grants the *in-process authority* (the key + // baked into the fullstack suite's InProcessAuthority) the + // default-authority role, but our driverAuth is a different address. + // Grant it too via the lower-level helper. sim := suite.GetSimTestHelper() sim.WaitMined(sim.AddAuthority(new(big.Int).SetUint64(uint64(appID)), driverAuth.UserAddress())) @@ -91,10 +92,11 @@ func TestDeanonymize_RealRoundTrip(t *testing.T) { reportID, err := driverAuth.RequestReport(t.Context(), "balances", "100 wei") require.NoError(t, err) - // Report is now persisted to the manager's reports dir (same path the - // authority service reads from — Phase 3 wiring) and the subgraph has - // the RequestCompleted entry for the /getreport endpoint's sanity check. - // Pull it down via the full HTTP + decrypt pipeline. + // Report is now persisted to the manager's reports dir (the same path + // the in-process authority service reads from — configured at suite + // setup) and the subgraph has the RequestCompleted entry for the + // /getreport endpoint's sanity check. Pull it down via the full HTTP + + // decrypt pipeline. reportIDHex := fmt.Sprintf("%x", reportID[:]) t.Logf("deanonymization request id: %s", reportIDHex) destPath := filepath.Join(t.TempDir(), "report.json") diff --git a/runtime/wasm-go/fullstack_tests/erc20_fullstack_test.go b/runtime/wasm-go/fullstack_tests/erc20_fullstack_test.go index 2afb943..ed4264a 100644 --- a/runtime/wasm-go/fullstack_tests/erc20_fullstack_test.go +++ b/runtime/wasm-go/fullstack_tests/erc20_fullstack_test.go @@ -12,10 +12,10 @@ import ( "github.com/stretchr/testify/require" ) -// TestERC20FullStack is the Phase 5 reference test: a full end-to-end ERC-20 -// flow that exercises every layer of the harness (real simulated chain, real -// Wasmtime payment-app, in-process subgraph, in-process authority, wallet -// driver). The flow mirrors the plan's 14 steps: +// TestERC20FullStack is the reference end-to-end ERC-20 flow test. It +// exercises every layer of the harness (real simulated chain, real Wasmtime +// payment-app, in-process subgraph, in-process authority, wallet driver) +// via 14 sequential steps: // // 1. Start the suite. // 2. Deploy MockERC20. @@ -94,9 +94,9 @@ func TestERC20FullStack(t *testing.T) { "user should hold the minted MOCK before deposit") // Step 10: pre-approve the ProcessorEndpoint to spend the user's MOCK. - // Option A of the Phase 5 flow — the wallet's submitRequest uses - // transferFrom and does not embed an EIP-2612 permit, so approval must be - // pre-staged. A permit-embedding wallet (option B) is future work. + // The wallet's submitRequest uses transferFrom and does not embed an + // EIP-2612 permit, so approval must be pre-staged. A permit-embedding + // wallet variant is future work. userOpts, err := suite.GetTransactOpts(user) require.NoError(t, err) sim.WaitMined(sim.ApproveERC20(userOpts, sim.ProcessorContractAddress, mintAmount)) @@ -155,5 +155,5 @@ func TestERC20FullStack(t *testing.T) { require.Equal(t, 0, remainingRaw.Cmp(privBal2), "remaining private balance should be 600 MOCK; got %s", privBal2.String()) - t.Log("Phase 5 ERC-20 full-stack flow passed") + t.Log("ERC-20 full-stack flow passed") } diff --git a/runtime/wasm-go/fullstack_tests/multi_app_isolation_test.go b/runtime/wasm-go/fullstack_tests/multi_app_isolation_test.go index 01a0697..10e757b 100644 --- a/runtime/wasm-go/fullstack_tests/multi_app_isolation_test.go +++ b/runtime/wasm-go/fullstack_tests/multi_app_isolation_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/require" ) -// TestMultiAppIsolation is the Tier 2 multi-tenancy test. It deploys two +// TestMultiAppIsolation is the multi-tenancy isolation test. It deploys two // independent payment applications on the same simulated chain, registers // the SAME user in both, and then verifies that every piece of state is // scoped per-appID and cannot bleed across the boundary. A regression that diff --git a/runtime/wasm-go/system_tests/payment_app_system_test.go b/runtime/wasm-go/system_tests/payment_app_system_test.go index f442bcc..bdff340 100644 --- a/runtime/wasm-go/system_tests/payment_app_system_test.go +++ b/runtime/wasm-go/system_tests/payment_app_system_test.go @@ -605,21 +605,24 @@ func TestPaymentAppERC20MultiToken(t *testing.T) { // seed, and events reach userA via the privacy-preserving hashed-subtype // set (WaitForEventBySubtypes with AllSubtypes(seed, N)). // - userB is NO-SEED: AssociateKey request carries only the P521 public -// key (133 bytes, no encrypted seed). Events reach userB via the plain -// subtype the guest emits (WaitForEvent with the literal subtype name). +// key (133 bytes, no encrypted seed). The payment-app emits PlainEvents +// with EventSubType == [32]byte{} (zero); with no seed, the executor +// preserves that zero value. Tests match via WaitForEvent with the +// zero-value subtype, which accepts any subtype (here, zero). // // The transfer goes A -> B, so we verify: -// - userA receives a "transfer_sent" event via the seed path, with correct +// - userA receives the sender event (JSON payload type "transfer_sent") +// via the seed path, with correct tokenAddress, amount, and +// post-transfer balance +// - userB receives the recipient event (JSON payload type +// "transfer_received") via the no-seed zero-subtype path, with correct // tokenAddress, amount, and post-transfer balance -// - userB receives a "transfer_received" event via the no-seed plain- -// subtype path, with correct tokenAddress, amount, and post-transfer -// balance // - the deanonymization report shows both accounts with the expected // balances // - total conservation: (userA remaining + userB remaining) == sum of the // two original deposits (no tokens minted or lost by the transfer) // -// The no-seed user receiving a transfer_received event is new coverage vs +// The no-seed user receiving a recipient event is new coverage vs // TestPaymentAppFullFlow, which only exercises the no-seed path for deposit // events. func TestPaymentAppERC20MultiUser(t *testing.T) { @@ -711,10 +714,12 @@ func TestPaymentAppERC20MultiUser(t *testing.T) { require.Equal(t, 0, expectedABalance.Cmp(senderData.Balance.ToInt()), "userA post-transfer balance: expected %s, got %s", expectedABalance, senderData.Balance.ToInt()) - // --- Verify recipient event (userB, no-seed plain-subtype path) --- - // The guest emits the recipient event with subtype "transfer_received"; - // with no seed registered the framework preserves that literal subtype. - recipientEvent, err := suite.WaitForEvent(userB, "transfer_received", timeout) + // --- Verify recipient event (userB, no-seed zero-subtype path) --- + // The payment-app sets EventSubType to [32]byte{} on PlainEvents (it + // relies on the executor's HMAC override when a seed is registered). + // With no seed, the executor preserves the zero value. Passing the + // zero-value subtype to WaitForEvent matches any subtype. + recipientEvent, err := suite.WaitForEvent(userB, [32]byte{}, timeout) require.NoError(t, err) recipientDecrypted, err := cryptoHelper.DecryptEvent(userB, recipientEvent, executorPubKey) require.NoError(t, err) diff --git a/wallet/testutil/wallet_driver.go b/wallet/testutil/wallet_driver.go index cb19414..57028fb 100644 --- a/wallet/testutil/wallet_driver.go +++ b/wallet/testutil/wallet_driver.go @@ -132,8 +132,10 @@ func newWalletDriverInternal( processor := sim.ProcessorContractAddress teeAuth := sim.TeeSignerAddress - // Seed the registry with just the implicit ETH entry. Phase 5 will add - // ERC-20 tokens by writing token.* conf entries before SaveConfigToFile. + // Seed the registry with just the implicit ETH entry. ERC-20 tokens are + // added later via WalletDriver.AddToken, which appends token.* entries + // to the on-disk conf; the next loadConfig() call rebuilds the registry + // from disk. tokens, err := app.LoadTokenRegistry(nil) require.NoError(t, err) diff --git a/wallet/testutil/wallet_driver_test.go b/wallet/testutil/wallet_driver_test.go index 8611854..b5786e3 100644 --- a/wallet/testutil/wallet_driver_test.go +++ b/wallet/testutil/wallet_driver_test.go @@ -14,12 +14,13 @@ import ( "github.com/stretchr/testify/require" ) -// TestWalletDriver_DeployRegisterDeposit is the Phase 4 end-to-end smoke. -// It drives three wallet commands (DeployApp, RegisterUser, Deposit) against -// a real simulated chain + in-process subgraph + in-process authority + -// mock-runtime executor. Assertions target on-chain custody and subgraph -// state rather than stdout — the driver returns structured errors, and the -// subgraph is the same source the wallet's WaitForRequestCompleted polls. +// TestWalletDriver_DeployRegisterDeposit is the end-to-end smoke for the +// WalletDriver harness: it drives three wallet commands (DeployApp, +// RegisterUser, Deposit) against a real simulated chain + in-process +// subgraph + in-process authority + mock-runtime executor. Assertions target +// on-chain custody and subgraph state rather than stdout — the driver +// returns structured errors, and the subgraph is the same source the +// wallet's WaitForRequestCompleted polls. // // The runtime is mock-runtime so no TinyGo build is required; any bytes are // accepted as the "WASM" artifact. This keeps the test hermetic. diff --git a/wallet/testutil/wrappers.go b/wallet/testutil/wrappers.go index 88a36ad..5a5c6ae 100644 --- a/wallet/testutil/wrappers.go +++ b/wallet/testutil/wrappers.go @@ -16,7 +16,8 @@ import ( // Returns the assigned ApplicationID. // // allowedTokens may be nil (ETH is always allowed). ERC-20 symbols require -// those tokens to be registered in the driver's conf; that lands with Phase 5. +// those tokens to be registered in the driver's conf first via +// WalletDriver.AddToken; otherwise resolution of the symbol here would fail. func (d *WalletDriver) DeployApp(ctx context.Context, wasmPath, maxFee string, allowedTokens []string) (common.ApplicationIdType, error) { d.t.Helper() From e9cfcbabcab1540b9d87d50d30f5198bb03d08c7 Mon Sep 17 00:00:00 2001 From: Alberto Sala Date: Fri, 24 Apr 2026 12:17:31 +0200 Subject: [PATCH 16/25] added full stack sys test covering coupled-restart keyset-recovery handshake and cross-restart state survival --- .../full_stack_restart_test.go | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 runtime/wasm-go/fullstack_tests/full_stack_restart_test.go diff --git a/runtime/wasm-go/fullstack_tests/full_stack_restart_test.go b/runtime/wasm-go/fullstack_tests/full_stack_restart_test.go new file mode 100644 index 0000000..21628cc --- /dev/null +++ b/runtime/wasm-go/fullstack_tests/full_stack_restart_test.go @@ -0,0 +1,164 @@ +package main_test + +import ( + "math/big" + "os" + "path/filepath" + "testing" + + "github.com/HorizenOfficial/vela-nova/payment-app/testhelpers" + walletTestutil "github.com/HorizenOfficial/vela-nova/wallet/testutil" + "github.com/HorizenOfficial/vela/pkg/testutil/fullstack" + ethCommon "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +// TestFullStackRestart_KeysetRecovery proves that encrypted application +// state survives a coupled restart of BOTH the manager and the executor. +// This is the production-realistic reboot scenario: host reboots, Docker +// brings up both services under `restart: always`, and the system must +// resume cooperation with no data loss. +// +// The key invariant under test is the Type-0 keyset-recovery handshake +// working end-to-end against a populated data layer — i.e., the recovery +// blob written by the initial executor at first startup (into the manager's +// versioned LevelDB) is successfully served back to a fresh executor +// instance, which restores its original keyset and can decrypt state that +// was encrypted pre-restart. +// +// Four layers of robustness are exercised in one flow: +// +// 1. Manager's data layer persistence of the keyset-recovery blob across +// the Manager instance's lifecycle (re-used struct pointer; the LevelDB +// files are NOT deleted between manager instances). +// 2. Communication channel re-establishment — fresh comm.Server on the +// same TCP port, fresh comm.Client from the rebuilt manager. +// 3. Executor's keyset restoration from the recovery blob (Type-0 handshake +// messages 4 → 5 → 8 runs against the stored data, not against fresh +// key generation). +// 4. Versioned LevelDB integrity: encrypted app state written pre-restart +// is decrypted and read post-restart using the same recovered keyset. +// +// Flow (9 steps): +// +// 1. Start suite + executor + manager. +// 2. Driver deploys app + registers user (keyset-recovery blob is now +// persisted in the manager's data layer). +// 3. Deposit 1 ETH. Assert private balance == 1 ETH — pre-restart state +// is established and encrypted under the initial executor keyset. +// 4. suite.RestartAll() — stops both manager + executor, rebuilds fresh +// instances over the preserved data layer, restarts. Fresh executor +// runs the Type-0 handshake on startup; fresh manager serves the +// stored recovery blob; executor restores its keyset. +// 5. Assert private balance STILL == 1 ETH. Proves the fresh executor +// restored the same keyset (otherwise it could not decrypt the stored +// state). +// 6. Deposit 0.5 ETH (new op post-restart). Exercises the full +// post-recovery write path against the restored keyset. +// 7. Assert private balance == 1.5 ETH. +// 8. Withdraw 0.3 ETH (post-restart mutation path). +// 9. Assert private balance == 1.2 ETH and appCustody[ETH] == 1.2 ETH. +// +// Non-goals: +// - Executor-only restart (the manager currently lacks reconnect logic; +// coupled restart is the only meaningful test in the current design). +// - Manager-only restart (symmetric same limitation). +// - Multiple consecutive restarts (one round-trip proves the mechanism; +// further iterations have diminishing value). +func TestFullStackRestart_KeysetRecovery(t *testing.T) { + if os.Getenv("CI_FLAG") != "" { + t.Skip("Skipping fullstack test in CI environment") + } + + t.Setenv("MANAGER_ARTIFACTS_PATH", t.TempDir()) + + suite := fullstack.NewFullStackSystemTestSuite(t, "wasmtime-payment", + testhelpers.NewNetworkLogConfig(), testhelpers.NewNetworkLogConfig()) + defer suite.Cleanup() + + // Step 1: initial startup. The executor generates a keyset and (via the + // first-time handshake) the manager stores the recovery blob in its + // data layer. That stored blob is what the restart will recover from. + require.NoError(t, suite.StartExecutor()) + require.NoError(t, suite.StartManager()) + + driver := walletTestutil.NewWalletDriver(t, suite) + + wasmBytes := testhelpers.BuildAndLoadWasmModule(t) + wasmPath := filepath.Join(t.TempDir(), "payment_app.wasm") + require.NoError(t, os.WriteFile(wasmPath, wasmBytes, 0o644)) + + // Step 2: app deploy + user registration. Registration submits the + // AssociateKey request; the executor encrypts the resulting state with + // the initial keyset. + appID, err := driver.DeployApp(t.Context(), wasmPath, "100 wei", nil) + require.NoError(t, err) + require.NotZero(t, appID) + require.NoError(t, driver.RegisterUser(t.Context(), "100 wei")) + + ethAddr := ethCommon.Address{} + oneEther := new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil) + halfEther := new(big.Int).Div(oneEther, big.NewInt(2)) + threeTenthsEther := new(big.Int).Mul(big.NewInt(3), new(big.Int).Exp(big.NewInt(10), big.NewInt(17), nil)) // 0.3 ETH + + // Step 3: deposit 1 ETH pre-restart. After this the encrypted app state + // contains the deposit and is persisted in the manager's data layer. + require.NoError(t, driver.Deposit(t.Context(), "1 ETH", "", "100 wei")) + + balPre, err := driver.GetPrivateBalance(t.Context(), "") + require.NoError(t, err) + require.NotNil(t, balPre, "pre-restart balance should be visible") + require.Equal(t, 0, oneEther.Cmp(balPre), + "pre-restart balance should be 1 ETH; got %s wei", balPre.String()) + + // Step 4: restart both sides. Data layer survives (same *LevelDB* + // instance reference), so the recovery blob is available to the fresh + // manager. Fresh executor's Type-0 handshake should find it and restore + // the same keyset that encrypted the deposit above. + require.NoError(t, suite.RestartAll(), "RestartAll should complete the Type-0 handshake and restore the original keyset") + + // Step 5: the critical assertion. If the fresh executor did NOT restore + // the original keyset (e.g., it generated a new one), it could not + // decrypt the state written by the pre-restart executor, and this read + // would either fail or return garbage / nil. + balRestored, err := driver.GetPrivateBalance(t.Context(), "") + require.NoError(t, err, "reading private balance after restart must not error") + require.NotNil(t, balRestored, "post-restart balance MUST be visible — if nil, the keyset was not restored and old state is unreadable") + require.Equal(t, 0, oneEther.Cmp(balRestored), + "post-restart balance should equal pre-restart value (1 ETH); got %s wei", balRestored.String()) + + // Step 6: new deposit post-restart. Exercises the full write path with + // the restored keyset: encrypt state under the restored comm key, sign + // with the restored signing key, teeAuthenticator on-chain still + // accepts because the signing address is unchanged. + require.NoError(t, driver.Deposit(t.Context(), "0.5 ETH", "", "100 wei")) + + // Step 7: cumulative balance. + wantBalAfterSecondDeposit := new(big.Int).Add(oneEther, halfEther) // 1.5 ETH + balAfterSecondDeposit, err := driver.GetPrivateBalance(t.Context(), "") + require.NoError(t, err) + require.NotNil(t, balAfterSecondDeposit) + require.Equal(t, 0, wantBalAfterSecondDeposit.Cmp(balAfterSecondDeposit), + "post-restart + second-deposit balance should be 1.5 ETH; got %s wei", balAfterSecondDeposit.String()) + + // Step 8: withdraw post-restart. Exercises a state-mutation path + // (read + debit + write) plus on-chain pendingClaims credit, all via + // the restored keyset. + userHex := driver.UserAddress().Hex() + require.NoError(t, driver.Withdraw(t.Context(), "0.3 ETH", userHex, "", "100 wei")) + + // Step 9: final state. Private balance reflects the debit; on-chain + // custody reflects deposits minus the withdrawn-to-pendingClaims amount. + wantBalFinal := new(big.Int).Sub(wantBalAfterSecondDeposit, threeTenthsEther) // 1.2 ETH + balFinal, err := driver.GetPrivateBalance(t.Context(), "") + require.NoError(t, err) + require.NotNil(t, balFinal) + require.Equal(t, 0, wantBalFinal.Cmp(balFinal), + "final private balance should be 1.2 ETH; got %s wei", balFinal.String()) + + custody := suite.GetAppCustody(appID, ethAddr) + require.Equal(t, 0, wantBalFinal.Cmp(custody), + "final on-chain custody should be 1.2 ETH (deposits minus withdrawn amount); got %s", custody.String()) + + t.Log("Full-stack restart + keyset recovery: pre-restart state readable post-restart; new operations succeed against the restored keyset") +} From 97b2a9c1baf9dde65209d565fd22926a290ba367 Mon Sep 17 00:00:00 2001 From: Alberto Sala Date: Fri, 24 Apr 2026 14:36:39 +0200 Subject: [PATCH 17/25] add fullstack sys test proving ProcessorEndpoint.InvalidSignature fires when on-chain TEE signer mismatches the executor's real signing key --- .../tee_attestation_rejection_test.go | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 runtime/wasm-go/fullstack_tests/tee_attestation_rejection_test.go diff --git a/runtime/wasm-go/fullstack_tests/tee_attestation_rejection_test.go b/runtime/wasm-go/fullstack_tests/tee_attestation_rejection_test.go new file mode 100644 index 0000000..4665ac0 --- /dev/null +++ b/runtime/wasm-go/fullstack_tests/tee_attestation_rejection_test.go @@ -0,0 +1,130 @@ +package main_test + +import ( + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/HorizenOfficial/vela-nova/payment-app/testhelpers" + walletTestutil "github.com/HorizenOfficial/vela-nova/wallet/testutil" + "github.com/HorizenOfficial/vela/pkg/testutil/fullstack" + ethCrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" +) + +// TestTEEAttestationRejection proves that ProcessorEndpoint's on-chain +// stateUpdate rejects a state update signed by a key that does NOT match +// the TEE signer address registered in the TeeAuthenticator contract. This +// is the security invariant underpinning the entire trust model — if the +// signature check were ever removed or short-circuited, a compromised +// manager could fabricate state updates without enclave authorisation, and +// the contract would accept them. +// +// Every other fullstack test deploys the mock authenticator pre-populated +// with the real executor's signing address, so the verify step trivially +// passes — a regression here would be invisible to those tests. This one +// flips the registered signer to a ROGUE address while leaving the +// executor's real signing key in place; the executor still signs state +// updates with its real key, those signatures recover to its real address, +// and the authenticator rejects them because that address != rogue. +// +// Fullstack-unique: +// - Hardhat unit tests cover the TeeAuthenticator contract in isolation. +// - Only fullstack proves the WIRING: manager actually calls stateUpdate, +// ProcessorEndpoint actually delegates to the authenticator, a signer +// mismatch actually surfaces as an on-chain revert visible to the +// manager's submission path. +// +// Design choices: +// - Rogue address registered at deploy time; executor keyset untouched. +// Simpler than tampering with a signature post-sign, and still exactly +// hits the verify branch we want to test. +// - Real comm pubkey kept on-chain. If we rogued that too, wallet +// payload encryption would fail at RegisterUser (well before reaching +// the signature check) — a false positive that wouldn't prove anything +// about the signature path. +// - DeployApp is the trigger: a single request round-trip produces a +// single stateUpdate submission. The revert manifests on that +// submission. The same revert would hit any other request type +// (deposit, withdraw, transfer) — all of them go through the same +// stateUpdate entry point. +// +// Flow (6 steps): +// +// 1. Generate a rogue secp256k1 key; compute its address. +// 2. Build the suite via NewFullStackSystemTestSuiteWithTeeSigner with +// the rogue address as the override. Start manager + executor. +// 3. Build the wallet driver (funded user + DEPLOYER_ROLE). +// 4. Kick off DeployApp in a goroutine — it will hang on the subgraph +// poll because the stateUpdate never completes. This is fine; the +// assertion doesn't wait on the driver, it waits on the underlying +// state-update error channel. +// 5. suite.WaitForStateUpdateError(...) returns the specific error +// (with a short budget — the revert reason should surface within a +// few seconds of the executor processing the deploy). +// 6. Assert the error string contains "InvalidSignature" — the custom +// error the contract reverts with when teeAuthenticator.checkSignature +// returns false. +func TestTEEAttestationRejection(t *testing.T) { + if os.Getenv("CI_FLAG") != "" { + t.Skip("Skipping fullstack test in CI environment") + } + + t.Setenv("MANAGER_ARTIFACTS_PATH", t.TempDir()) + + // Step 1: rogue signer address. Distinct from whatever the executor + // generates; the registered signer on-chain will be this address, but + // the executor will sign with its own real key. + rogueKey, err := ethCrypto.GenerateKey() + require.NoError(t, err) + rogueAddr := ethCrypto.PubkeyToAddress(rogueKey.PublicKey) + + // Step 2: suite wiring with the rogue override. + suite := fullstack.NewFullStackSystemTestSuiteWithTeeSigner( + t, "wasmtime-payment", + testhelpers.NewNetworkLogConfig(), testhelpers.NewNetworkLogConfig(), + &rogueAddr, + ) + defer suite.Cleanup() + + require.NoError(t, suite.StartExecutor()) + require.NoError(t, suite.StartManager()) + + // Step 3: driver (funded user with DEPLOYER_ROLE). + driver := walletTestutil.NewWalletDriver(t, suite) + + wasmBytes := testhelpers.BuildAndLoadWasmModule(t) + wasmPath := filepath.Join(t.TempDir(), "payment_app.wasm") + require.NoError(t, os.WriteFile(wasmPath, wasmBytes, 0o644)) + + // Step 4: kick off the deploy in a background goroutine. It will + // never complete (subgraph never sees RequestCompleted because the + // stateUpdate keeps reverting), so we don't wait on its result — + // we assert on the captured state-update error instead. + deployDone := make(chan error, 1) + go func() { + _, err := driver.DeployApp(t.Context(), wasmPath, "100 wei", nil) + deployDone <- err + }() + + // Step 5 + 6: wait for the specific revert. The manager submits the + // stateUpdate once the executor finishes processing the deploy + // request, which happens within a few seconds of the on-chain + // DeployRequest submission. + stateUpdateErr, arrived := suite.WaitForStateUpdateError(30 * time.Second) + require.True(t, arrived, "no state-update error arrived within 30s — the manager did not attempt stateUpdate, or the channel was full (unlikely with buffer=16)") + require.Error(t, stateUpdateErr) + require.True(t, + strings.Contains(stateUpdateErr.Error(), "InvalidSignature"), + "expected on-chain revert reason to mention InvalidSignature "+ + "(ProcessorEndpoint's custom error when teeAuthenticator.checkSignature returns false); got: %v", + stateUpdateErr, + ) + + t.Logf("stateUpdate correctly rejected with: %v", stateUpdateErr) + // The goroutine is still blocked on the driver's polling timeout. + // Let it continue to exit cleanly on suite.Cleanup cancellation. + _ = deployDone +} From d55d2f3c575cb5d5a2f82e2f8503b3fee4b47e95 Mon Sep 17 00:00:00 2001 From: Alberto Sala Date: Fri, 24 Apr 2026 14:59:11 +0200 Subject: [PATCH 18/25] Added AssertNoStateUpdateErrors safety-net assertion to all happy-path fullstack tests (catches silent retry-and-recover regressions) --- .../wasm-go/fullstack_tests/deanonymize_test.go | 1 + .../fullstack_tests/erc20_fullstack_test.go | 1 + .../fullstack_tests/full_stack_restart_test.go | 1 + .../fullstack_tests/multi_app_isolation_test.go | 1 + .../fullstack_tests/payment_app_fullstack_test.go | 1 + .../private_transfer_erc20_test.go | 1 + .../fullstack_tests/private_transfer_test.go | 1 + wallet/cmd/claimpendingpayments_test.go | 6 ++++-- wallet/cmd/decryptreport_test.go | 3 ++- wallet/cmd/deployapp_test.go | 3 ++- wallet/cmd/deposit_test.go | 15 ++++++++++----- wallet/cmd/getpendingpayments_test.go | 6 ++++-- wallet/cmd/getpublicbalance_test.go | 9 ++++++--- wallet/cmd/privatetransfer_test.go | 3 ++- wallet/cmd/registeruser_test.go | 9 ++++++--- wallet/cmd/requestreport_test.go | 3 ++- wallet/cmd/withdraw_test.go | 6 ++++-- 17 files changed, 49 insertions(+), 21 deletions(-) diff --git a/runtime/wasm-go/fullstack_tests/deanonymize_test.go b/runtime/wasm-go/fullstack_tests/deanonymize_test.go index 9e095a2..1e409c5 100644 --- a/runtime/wasm-go/fullstack_tests/deanonymize_test.go +++ b/runtime/wasm-go/fullstack_tests/deanonymize_test.go @@ -131,5 +131,6 @@ func TestDeanonymize_RealRoundTrip(t *testing.T) { strings.ToLower(driverUser.UserAddress().Hex()), "balances report should mention the depositing user; raw=%s", string(got.ReportData)) + suite.AssertNoStateUpdateErrors(t) t.Log("Real deanonymize /getreport round-trip verified") } diff --git a/runtime/wasm-go/fullstack_tests/erc20_fullstack_test.go b/runtime/wasm-go/fullstack_tests/erc20_fullstack_test.go index ed4264a..324e83e 100644 --- a/runtime/wasm-go/fullstack_tests/erc20_fullstack_test.go +++ b/runtime/wasm-go/fullstack_tests/erc20_fullstack_test.go @@ -155,5 +155,6 @@ func TestERC20FullStack(t *testing.T) { require.Equal(t, 0, remainingRaw.Cmp(privBal2), "remaining private balance should be 600 MOCK; got %s", privBal2.String()) + suite.AssertNoStateUpdateErrors(t) t.Log("ERC-20 full-stack flow passed") } diff --git a/runtime/wasm-go/fullstack_tests/full_stack_restart_test.go b/runtime/wasm-go/fullstack_tests/full_stack_restart_test.go index 21628cc..64fdbd1 100644 --- a/runtime/wasm-go/fullstack_tests/full_stack_restart_test.go +++ b/runtime/wasm-go/fullstack_tests/full_stack_restart_test.go @@ -160,5 +160,6 @@ func TestFullStackRestart_KeysetRecovery(t *testing.T) { require.Equal(t, 0, wantBalFinal.Cmp(custody), "final on-chain custody should be 1.2 ETH (deposits minus withdrawn amount); got %s", custody.String()) + suite.AssertNoStateUpdateErrors(t) t.Log("Full-stack restart + keyset recovery: pre-restart state readable post-restart; new operations succeed against the restored keyset") } diff --git a/runtime/wasm-go/fullstack_tests/multi_app_isolation_test.go b/runtime/wasm-go/fullstack_tests/multi_app_isolation_test.go index 10e757b..c8385ff 100644 --- a/runtime/wasm-go/fullstack_tests/multi_app_isolation_test.go +++ b/runtime/wasm-go/fullstack_tests/multi_app_isolation_test.go @@ -231,5 +231,6 @@ func TestMultiAppIsolation(t *testing.T) { require.Equal(t, 0, halfEther.Cmp(balB3), "B's private balance must remain 0.5 ETH — A's withdraw perturbed B's view") + suite.AssertNoStateUpdateErrors(t) t.Log("Multi-app isolation verified: on-chain custody, subgraph event filter, executor state, and private-balance decrypt all scope correctly by appID") } diff --git a/runtime/wasm-go/fullstack_tests/payment_app_fullstack_test.go b/runtime/wasm-go/fullstack_tests/payment_app_fullstack_test.go index bf549ba..e7941a1 100644 --- a/runtime/wasm-go/fullstack_tests/payment_app_fullstack_test.go +++ b/runtime/wasm-go/fullstack_tests/payment_app_fullstack_test.go @@ -164,6 +164,7 @@ func TestFullStackDeployAndDeposit(t *testing.T) { require.Equal(t, 0, depositAmount.Cmp(sgEventData.Amount.ToInt()), "subgraph event amount: expected %s, got %s", depositAmount, sgEventData.Amount.ToInt()) + suite.AssertNoStateUpdateErrors(t) t.Log("Fullstack deploy + deposit + subgraph verification passed") } diff --git a/runtime/wasm-go/fullstack_tests/private_transfer_erc20_test.go b/runtime/wasm-go/fullstack_tests/private_transfer_erc20_test.go index da12e4c..fda6e43 100644 --- a/runtime/wasm-go/fullstack_tests/private_transfer_erc20_test.go +++ b/runtime/wasm-go/fullstack_tests/private_transfer_erc20_test.go @@ -167,5 +167,6 @@ func TestPrivateTransfer_TwoUsers_ERC20(t *testing.T) { require.NoError(t, err) require.Nil(t, balBEth, "B should have no ETH private balance — A holds ETH but never transferred any to B") + suite.AssertNoStateUpdateErrors(t) t.Log("ERC-20 private transfer with multi-token isolation verified: non-18 decimals, per-token filter, per-user routing") } diff --git a/runtime/wasm-go/fullstack_tests/private_transfer_test.go b/runtime/wasm-go/fullstack_tests/private_transfer_test.go index 3d71f8e..e6f2d68 100644 --- a/runtime/wasm-go/fullstack_tests/private_transfer_test.go +++ b/runtime/wasm-go/fullstack_tests/private_transfer_test.go @@ -105,5 +105,6 @@ func TestPrivateTransfer_TwoUsers(t *testing.T) { require.Equal(t, 0, transferred.Cmp(balB), "B's post-transfer balance should be 0.3 ETH; got %s wei", balB.String()) + suite.AssertNoStateUpdateErrors(t) t.Log("Private transfer between two users — balances, subgraph indexing, and per-user decryption all verified") } diff --git a/wallet/cmd/claimpendingpayments_test.go b/wallet/cmd/claimpendingpayments_test.go index 2f0228f..dae7d2e 100644 --- a/wallet/cmd/claimpendingpayments_test.go +++ b/wallet/cmd/claimpendingpayments_test.go @@ -25,7 +25,8 @@ func TestClaimPendingPaymentsCmd(t *testing.T) { key1, err := crypto.GeneratePrivateKeySecp256k1() require.NoError(t, err) - testHelper := pestestutil.NewSimTestHelper(t, true, true, nil, nil) + autoMining, useMockContracts := true, true + testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, nil) defer testHelper.Close() var blockchainClient blockchain.Client = testutil.SetupNewBlockChainClient(testHelper) @@ -55,7 +56,8 @@ func TestClaimPendingPaymentsCmdNoKey(t *testing.T) { require.NoError(t, err) os.Stdout = w - testHelper := pestestutil.NewSimTestHelper(t, true, true, nil, nil) + autoMining, useMockContracts := true, true + testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, nil) defer testHelper.Close() var blockchainClient blockchain.Client = testutil.SetupNewBlockChainClient(testHelper) diff --git a/wallet/cmd/decryptreport_test.go b/wallet/cmd/decryptreport_test.go index 119b72b..ba9a630 100644 --- a/wallet/cmd/decryptreport_test.go +++ b/wallet/cmd/decryptreport_test.go @@ -64,7 +64,8 @@ func TestDecryptReport(t *testing.T) { os.Stdout = w //create client - testHelper := pestestutil.NewSimTestHelper(t, true, true, nil, teeKey.PublicKey().Bytes()) + autoMining, useMockContracts := true, true + testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, teeKey.PublicKey().Bytes()) defer testHelper.Close() client := testutil.SetupNewBlockChainClient(testHelper) diff --git a/wallet/cmd/deployapp_test.go b/wallet/cmd/deployapp_test.go index f59e01e..5c586d6 100644 --- a/wallet/cmd/deployapp_test.go +++ b/wallet/cmd/deployapp_test.go @@ -338,7 +338,8 @@ func TestDeployAppCommand_UnauthorizedDeployerReverts(t *testing.T) { })) defer artifactServer.Close() - testHelper := velatestutil.NewSimTestHelper(t, true, true, nil, nil) + autoMining, useMockContracts := true, true + testHelper := velatestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, nil) defer testHelper.Close() // Create a blockchain client using the Submitter account, which does NOT have diff --git a/wallet/cmd/deposit_test.go b/wallet/cmd/deposit_test.go index f65e887..95a1581 100644 --- a/wallet/cmd/deposit_test.go +++ b/wallet/cmd/deposit_test.go @@ -30,7 +30,8 @@ func TestDepositCmdInvalidDepositAmount(t *testing.T) { key2, err := crypto.GeneratePrivateKeyP521() require.NoError(t, err) - testHelper := pestestutil.NewSimTestHelper(t, true, true, nil, nil) + autoMining, useMockContracts := true, true + testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, nil) defer testHelper.Close() var blockchainClient blockchain.Client = testutil.SetupNewBlockChainClient(testHelper) @@ -77,7 +78,8 @@ func TestDepositCmdInvalidMaxValueFee(t *testing.T) { key2, err := crypto.GeneratePrivateKeyP521() require.NoError(t, err) - testHelper := pestestutil.NewSimTestHelper(t, true, true, nil, nil) + autoMining, useMockContracts := true, true + testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, nil) defer testHelper.Close() var blockchainClient blockchain.Client = testutil.SetupNewBlockChainClient(testHelper) @@ -124,7 +126,8 @@ func TestDepositCmd(t *testing.T) { key2, err := crypto.GeneratePrivateKeyP521() require.NoError(t, err) - testHelper := pestestutil.NewSimTestHelper(t, true, true, nil, nil) + autoMining, useMockContracts := true, true + testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, nil) defer testHelper.Close() appID := testutil.DeployTestApplication(t, testHelper) @@ -176,7 +179,8 @@ func TestDepositCmdFailure(t *testing.T) { key2, err := crypto.GeneratePrivateKeyP521() require.NoError(t, err) - testHelper := pestestutil.NewSimTestHelper(t, true, true, nil, nil) + autoMining, useMockContracts := true, true + testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, nil) defer testHelper.Close() appID := testutil.DeployTestApplication(t, testHelper) @@ -227,7 +231,8 @@ func TestDepositCmdUsesDefaultMaxValueFee(t *testing.T) { key2, err := crypto.GeneratePrivateKeyP521() require.NoError(t, err) - testHelper := pestestutil.NewSimTestHelper(t, true, true, nil, nil) + autoMining, useMockContracts := true, true + testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, nil) defer testHelper.Close() appID := testutil.DeployTestApplication(t, testHelper) diff --git a/wallet/cmd/getpendingpayments_test.go b/wallet/cmd/getpendingpayments_test.go index 981c020..49eeecf 100644 --- a/wallet/cmd/getpendingpayments_test.go +++ b/wallet/cmd/getpendingpayments_test.go @@ -25,7 +25,8 @@ func TestGetPendingPaymentsCmd(t *testing.T) { key1, err := crypto.GeneratePrivateKeySecp256k1() require.NoError(t, err) - testHelper := pestestutil.NewSimTestHelper(t, true, true, nil, nil) + autoMining, useMockContracts := true, true + testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, nil) defer testHelper.Close() var blockchainClient blockchain.Client = testutil.SetupNewBlockChainClient(testHelper) @@ -55,7 +56,8 @@ func TestGetPendingPaymentsCmdNoKey(t *testing.T) { require.NoError(t, err) os.Stdout = w - testHelper := pestestutil.NewSimTestHelper(t, true, true, nil, nil) + autoMining, useMockContracts := true, true + testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, nil) defer testHelper.Close() var blockchainClient blockchain.Client = testutil.SetupNewBlockChainClient(testHelper) diff --git a/wallet/cmd/getpublicbalance_test.go b/wallet/cmd/getpublicbalance_test.go index a593398..9e8c582 100644 --- a/wallet/cmd/getpublicbalance_test.go +++ b/wallet/cmd/getpublicbalance_test.go @@ -27,7 +27,8 @@ func TestGetPublicBalanceCmd_ZeroBalance(t *testing.T) { os.Stdout = w defer func() { os.Stdout = old }() - testHelper := pestestutil.NewSimTestHelper(t, true, true, nil, nil) + autoMining, useMockContracts := true, true + testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, nil) defer testHelper.Close() key, err := crypto.GeneratePrivateKeySecp256k1() @@ -55,7 +56,8 @@ func TestGetPublicBalanceCmd_FundedBalance(t *testing.T) { os.Stdout = w defer func() { os.Stdout = old }() - testHelper := pestestutil.NewSimTestHelper(t, true, true, nil, nil) + autoMining, useMockContracts := true, true + testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, nil) defer testHelper.Close() key, err := crypto.GeneratePrivateKeySecp256k1() @@ -83,7 +85,8 @@ func TestGetPublicBalanceCmd_FundedBalance(t *testing.T) { // non-18 decimals keep the test honest — a hardcoded 10^18 divisor in the // format path would surface here as the wrong number of whole tokens. func TestGetPublicBalanceCmd_ERC20_Balance(t *testing.T) { - testHelper := pestestutil.NewSimTestHelper(t, true, true, nil, nil) + autoMining, useMockContracts := true, true + testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, nil) defer testHelper.Close() const mockDecimals = 6 diff --git a/wallet/cmd/privatetransfer_test.go b/wallet/cmd/privatetransfer_test.go index be53912..dfa17b2 100644 --- a/wallet/cmd/privatetransfer_test.go +++ b/wallet/cmd/privatetransfer_test.go @@ -28,7 +28,8 @@ func TestPrivateTransfer(t *testing.T) { var key2, _ = crypto.GeneratePrivateKeyP521() var teeKey, _ = crypto.GeneratePrivateKeyP521() - testHelper := pestestutil.NewSimTestHelper(t, true, true, nil, teeKey.PublicKey().Bytes()) + autoMining, useMockContracts := true, true + testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, teeKey.PublicKey().Bytes()) defer testHelper.Close() appID := testutil.DeployTestApplication(t, testHelper) diff --git a/wallet/cmd/registeruser_test.go b/wallet/cmd/registeruser_test.go index a3ab5ae..9245a18 100644 --- a/wallet/cmd/registeruser_test.go +++ b/wallet/cmd/registeruser_test.go @@ -27,7 +27,8 @@ func TestRegisterUserCmd(t *testing.T) { var key2, _ = crypto.GeneratePrivateKeyP521() teeKey, _ := crypto.GeneratePrivateKeyP521() - testHelper := pestestutil.NewSimTestHelper(t, true, true, nil, teeKey.PublicKey().Bytes()) + autoMining, useMockContracts := true, true + testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, teeKey.PublicKey().Bytes()) defer testHelper.Close() appID := testutil.DeployTestApplication(t, testHelper) @@ -78,7 +79,8 @@ func TestRegisterUserCmdFailure(t *testing.T) { teeKey, err := crypto.GeneratePrivateKeyP521() require.NoError(t, err) - testHelper := pestestutil.NewSimTestHelper(t, true, true, nil, teeKey.PublicKey().Bytes()) + autoMining, useMockContracts := true, true + testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, teeKey.PublicKey().Bytes()) defer testHelper.Close() appID := testutil.DeployTestApplication(t, testHelper) @@ -129,7 +131,8 @@ func TestRegisterUserCmdTimeout(t *testing.T) { teeKey, err := crypto.GeneratePrivateKeyP521() require.NoError(t, err) - testHelper := pestestutil.NewSimTestHelper(t, true, true, nil, teeKey.PublicKey().Bytes()) + autoMining, useMockContracts := true, true + testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, teeKey.PublicKey().Bytes()) defer testHelper.Close() appID := testutil.DeployTestApplication(t, testHelper) diff --git a/wallet/cmd/requestreport_test.go b/wallet/cmd/requestreport_test.go index 8f5c699..411eb03 100644 --- a/wallet/cmd/requestreport_test.go +++ b/wallet/cmd/requestreport_test.go @@ -20,7 +20,8 @@ import ( func TestRequestReportCmd(t *testing.T) { teeKey, err := crypto.GeneratePrivateKeyP521() require.NoError(t, err) - testHelper := pestestutil.NewSimTestHelper(t, true, true, nil, teeKey.PublicKey().Bytes()) + autoMining, useMockContracts := true, true + testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, teeKey.PublicKey().Bytes()) defer testHelper.Close() appID := testutil.DeployTestApplication(t, testHelper) diff --git a/wallet/cmd/withdraw_test.go b/wallet/cmd/withdraw_test.go index 7654085..9caf0cc 100644 --- a/wallet/cmd/withdraw_test.go +++ b/wallet/cmd/withdraw_test.go @@ -32,7 +32,8 @@ func TestWithdrawCmd(t *testing.T) { teeKey, err := crypto.GeneratePrivateKeyP521() require.NoError(t, err) - testHelper := pestestutil.NewSimTestHelper(t, true, true, nil, teeKey.PublicKey().Bytes()) + autoMining, useMockContracts := true, true + testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, teeKey.PublicKey().Bytes()) defer testHelper.Close() appID := testutil.DeployTestApplication(t, testHelper) @@ -87,7 +88,8 @@ func TestWithdrawCmdFailure(t *testing.T) { teeKey, err := crypto.GeneratePrivateKeyP521() require.NoError(t, err) - testHelper := pestestutil.NewSimTestHelper(t, true, true, nil, teeKey.PublicKey().Bytes()) + autoMining, useMockContracts := true, true + testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, teeKey.PublicKey().Bytes()) defer testHelper.Close() appID := testutil.DeployTestApplication(t, testHelper) From 34e9cf048befacc8444cb304065765cb4c4ba1cf Mon Sep 17 00:00:00 2001 From: Alberto Sala Date: Fri, 24 Apr 2026 15:22:51 +0200 Subject: [PATCH 19/25] More fullstack system tests --- .../non_manager_state_update_test.go | 123 ++++++++++++ .../private_transfer_unregistered_test.go | 182 ++++++++++++++++++ 2 files changed, 305 insertions(+) create mode 100644 runtime/wasm-go/fullstack_tests/non_manager_state_update_test.go create mode 100644 runtime/wasm-go/fullstack_tests/private_transfer_unregistered_test.go diff --git a/runtime/wasm-go/fullstack_tests/non_manager_state_update_test.go b/runtime/wasm-go/fullstack_tests/non_manager_state_update_test.go new file mode 100644 index 0000000..6ace669 --- /dev/null +++ b/runtime/wasm-go/fullstack_tests/non_manager_state_update_test.go @@ -0,0 +1,123 @@ +package main_test + +import ( + "os" + "strings" + "testing" + + "github.com/HorizenOfficial/vela-nova/payment-app/testhelpers" + "github.com/HorizenOfficial/vela/pkg/common" + "github.com/HorizenOfficial/vela/pkg/testutil/fullstack" + "github.com/stretchr/testify/require" +) + +// TestNonManagerStateUpdateReverts proves that ProcessorEndpoint.stateUpdate +// is gated by UPDATE_STATUS_ROLE and rejects any caller other than the +// manager account (the sole role-holder granted at contract construction). +// If that gate were ever removed or relaxed, any funded account could +// forge state updates on-chain — catastrophic. +// +// Sibling of TestTEEAttestationRejection (S2): +// - S2 proves the *signature* embedded in the state update is verified. +// - This test proves the *caller* of stateUpdate is authorised. +// +// Both guards must hold for the security model to work; each is independent +// of the other. A regression on either would pass all other fullstack tests +// silently. +// +// Fullstack-unique: +// - OpenZeppelin's AccessControl is tested in isolation by its own unit +// tests and hardhat contract tests. +// - Only fullstack proves the WIRING: that the onlyRole modifier is +// actually attached to stateUpdate, that the manager's account is the +// grant target at construction, and that the revert selector surfaces +// through the Go unpacker as expected. +// +// Design: +// - Direct contract call from an unauthorised account. No executor or +// manager needed — the role check fires before any payload validation, +// so we can pass arbitrary bytes and still trigger the exact revert +// we want. +// - Fresh funded account via suite.CreateFundedAccount(): 5 ETH, zero +// roles on the contract, no special setup required. +// - The arbitrary UpdatePayload never reaches downstream checks (signature, +// state-root, request ID) because the role modifier short-circuits first. +// +// Flow: +// +// 1. Start the suite (no executor/manager — saves ~5s; test is ~2s). +// 2. Create a fresh funded rogue account (5 ETH, no UPDATE_STATUS_ROLE). +// 3. Build a minimally-typed UpdatePayload with arbitrary field values. +// 4. suite.SubmitStateUpdateAs(rogueKey, payload) — direct contract call +// signed by the rogue account, bypassing the manager and the wrapped +// eventBroadcastingClient. +// 5. Assert the returned error contains "AccessControlUnauthorizedAccount" +// (OZ's standard revert selector when the onlyRole modifier fails). +// 6. Cross-check: suite.AssertNoStateUpdateErrors(t) confirms the wrapped +// eventBroadcastingClient's buffer is empty — verifies that +// SubmitStateUpdateAs truly bypassed the wrapper (if a future +// refactor accidentally routed through it, the buffer would now hold +// an error and this assertion would flip the test red, catching the +// rewiring regression). +func TestNonManagerStateUpdateReverts(t *testing.T) { + if os.Getenv("CI_FLAG") != "" { + t.Skip("Skipping fullstack test in CI environment") + } + + t.Setenv("MANAGER_ARTIFACTS_PATH", t.TempDir()) + + // Step 1: suite start. No need to StartExecutor / StartManager — the + // contract role check is purely on-chain and fires before any off-chain + // processing is involved. + suite := fullstack.NewFullStackSystemTestSuite(t, "wasmtime-payment", + testhelpers.NewNetworkLogConfig(), testhelpers.NewNetworkLogConfig()) + defer suite.Cleanup() + + // Step 2: fresh funded account, unambiguously unauthorised. Only the + // manager account (set at ProcessorEndpoint construction via + // updateStatusOperator) holds UPDATE_STATUS_ROLE. + _, rogueKey, err := suite.CreateFundedAccount() + require.NoError(t, err) + + // Step 3: arbitrary-but-typed payload. The role modifier reverts + // before any field validation, so none of these values matter for + // the assertion — we just need a struct that the Go binding accepts + // when packing the calldata. + update := &common.UpdatePayload{ + ApplicationID: common.NewApplicationId(1), + RequestID: common.RequestIdType{}, + PrevStateRoot: [32]byte{}, + NewStateRoot: [32]byte{}, + Events: []common.Event{}, + AppEvents: []common.AppEvent{}, + Withdrawals: []common.Withdrawal{}, + Signature: []byte{}, + RefundAmount: common.NewBig(0), + ApplicationFee: common.NewBig(0), + ErrorCode: 0, + ErrorMsg: "", + } + + // Step 4 + 5: direct contract call signed by the rogue account. + // Expect the OZ AccessControl revert. + err = suite.SubmitStateUpdateAs(rogueKey, update) + require.Error(t, err, "stateUpdate from non-manager account must be rejected") + require.True(t, + strings.Contains(err.Error(), "AccessControlUnauthorizedAccount"), + "expected OZ AccessControlUnauthorizedAccount revert selector "+ + "(stateUpdate's onlyRole(UPDATE_STATUS_ROLE) gate); got: %v", + err, + ) + + // Step 6: sanity cross-check. SubmitStateUpdateAs builds a fresh + // blockchain.Client that does NOT go through the suite's wrapped + // eventBroadcastingClient, so its stateUpdateErrors buffer should + // stay empty. If a future refactor accidentally routed + // SubmitStateUpdateAs through the wrapper, this assertion would + // flip red — flagging the rewiring regression before it can + // silently corrupt other negative-path tests that rely on the + // buffer being a manager-only channel. + suite.AssertNoStateUpdateErrors(t) + + t.Logf("stateUpdate correctly rejected with: %v", err) +} diff --git a/runtime/wasm-go/fullstack_tests/private_transfer_unregistered_test.go b/runtime/wasm-go/fullstack_tests/private_transfer_unregistered_test.go new file mode 100644 index 0000000..a028fcf --- /dev/null +++ b/runtime/wasm-go/fullstack_tests/private_transfer_unregistered_test.go @@ -0,0 +1,182 @@ +package main_test + +import ( + "math/big" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/HorizenOfficial/vela-nova/payment-app/testhelpers" + walletTestutil "github.com/HorizenOfficial/vela-nova/wallet/testutil" + "github.com/HorizenOfficial/vela/pkg/testutil/fullstack" + ethCommon "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +// TestPrivateTransferToUnregisteredRecipient proves that a private transfer +// to a recipient who has NOT associated their P521 key on-chain fails +// atomically — the sender's balance is rolled back, no events are emitted, +// and no metadata about the attempted transfer surfaces on-chain or in the +// subgraph. The failure is surfaced via the request's errorCode (not a +// contract revert), so the stateUpdate transaction itself succeeds. +// +// ## Why this matters +// +// The payment-app's `Transfer` path generates two PlainEvents (sender's +// "transfer_sent" and recipient's "transfer_received"). The executor then +// tries to encrypt each event against the recipient's P521 pubkey from its +// keystore. If the recipient was never registered via RegisterUser, the +// keystore lookup fails with CodePubKeyNotRegistered, and the ENTIRE +// request is marked failed — state changes are rolled back and no events +// are emitted. +// +// Regressions that this test catches: +// +// - **Partial-commit bug:** sender's debit persists despite encryption +// failure. Would manifest as A's post-failure balance < 1 ETH. +// - **Metadata leak via failure receipt:** if a future change emitted an +// unencrypted "transfer_failed" event carrying sender + amount, the +// subgraph observer learns about the attempt. We assert B never sees +// anything and A's state is untouched — any surfacing of the transfer +// details on-chain would break those invariants. +// - **Unauthenticated auto-registration:** if the executor ever silently +// created a new keystore entry on-the-fly, the transfer would succeed +// but with a key the recipient doesn't control (silent fund loss). We +// assert the transfer fails, not succeeds. +// +// ## Fullstack-unique +// +// - Mocks stub the executor's keystore; only fullstack exercises the real +// encryptEvents → CodePubKeyNotRegistered path end-to-end. +// - Mocks also stub state-rollback on failure; only fullstack proves the +// real executor rolls back cleanly (sender not debited). +// +// ## Flow +// +// 1. Start suite + executor + manager. +// 2. Two wallet drivers, A and B. Both have funded on-chain accounts and +// fresh P521 keys inside their own configs — but that key has NOT yet +// been associated on-chain for B. +// 3. A deploys the payment-app. B.SetApplicationID(appID) — B points its +// conf at the same app but STILL does not register. +// 4. ONLY A calls RegisterUser. B is deliberately left unregistered. +// 5. A deposits 1 ETH. Assert A's private balance == 1 ETH (pre-condition). +// 6. A attempts a private transfer of 0.3 ETH to B's address. The wallet +// submits the request on-chain (no client-side pre-flight check exists +// — the sender has no visibility into B's registration status), the +// manager forwards it to the executor, the executor fails to encrypt +// the recipient event, and the request is marked failed. +// 7. Post-failure invariants (all MUST hold): +// a. PrivateTransfer returned an error ("request has failed" from +// the wrapper's waitForRequestCompletion path). +// b. A's private balance is UNCHANGED at 1 ETH — rollback held. +// c. A's on-chain appCustody is UNCHANGED at 1 ETH — deposit intact. +// d. B's private balance is nil — unregistered; also, no event +// indexable to B's address landed in the subgraph. +// e. AssertNoStateUpdateErrors — the failure is handled via +// errorCode, not a revert, so the wrapper's error channel stays +// empty. Any revert would indicate an unexpected on-chain path. +func TestPrivateTransferToUnregisteredRecipient(t *testing.T) { + if os.Getenv("CI_FLAG") != "" { + t.Skip("Skipping fullstack test in CI environment") + } + + t.Setenv("MANAGER_ARTIFACTS_PATH", t.TempDir()) + + // Step 1. + suite := fullstack.NewFullStackSystemTestSuite(t, "wasmtime-payment", + testhelpers.NewNetworkLogConfig(), testhelpers.NewNetworkLogConfig()) + defer suite.Cleanup() + + require.NoError(t, suite.StartExecutor()) + require.NoError(t, suite.StartManager()) + + // Step 2: both drivers built identically, both funded; the ONLY + // asymmetry is step 4 (A registers, B does not). + driverA := walletTestutil.NewWalletDriver(t, suite) + driverB := walletTestutil.NewWalletDriver(t, suite) + + // Step 3: A deploys, B is pointed at the same app. + wasmBytes := testhelpers.BuildAndLoadWasmModule(t) + wasmPath := filepath.Join(t.TempDir(), "payment_app.wasm") + require.NoError(t, os.WriteFile(wasmPath, wasmBytes, 0o644)) + + appID, err := driverA.DeployApp(t.Context(), wasmPath, "100 wei", nil) + require.NoError(t, err) + require.NotZero(t, appID) + driverB.SetApplicationID(appID) + + // Step 4: register A only. This is the critical asymmetry — the whole + // point of the test. If B were registered, the executor's keystore + // would resolve B's address to B's P521 pubkey and the transfer would + // succeed (that's TestPrivateTransfer_TwoUsers). + require.NoError(t, driverA.RegisterUser(t.Context(), "100 wei")) + // deliberately NOT: driverB.RegisterUser(...) + + // Step 5: establish the pre-condition — A has funds to attempt to send. + ethAddr := ethCommon.Address{} + oneEther := new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil) + require.NoError(t, driverA.Deposit(t.Context(), "1 ETH", "", "100 wei")) + + balAPre, err := driverA.GetPrivateBalance(t.Context(), "") + require.NoError(t, err) + require.NotNil(t, balAPre, "A's pre-transfer balance should be visible") + require.Equal(t, 0, oneEther.Cmp(balAPre), + "pre-transfer: A should have 1 ETH; got %s wei", balAPre.String()) + require.Equal(t, 0, oneEther.Cmp(suite.GetAppCustody(appID, ethAddr)), + "pre-transfer: appCustody should reflect the 1 ETH deposit") + + // Step 6: attempt the transfer. B has no P521 pubkey on-chain, so + // the executor's encryptEvents step will fail with CodePubKeyNotRegistered, + // the request is marked failed, and the wallet's polling observes the + // failure and surfaces it as an error. + err = driverA.PrivateTransfer(t.Context(), + driverB.UserAddress().Hex(), "0.3 ETH", "", "100 wei") + require.Error(t, err, "private transfer to unregistered recipient must fail") + // The executor's encryptEvents returns CodePubKeyNotRegistered (numeric + // code 9) when the recipient's P521 pubkey is missing from the keystore. + // That code is embedded in the signed error payload the manager records + // on-chain and surfaced by the wallet as "request failed (code 9)". + // Asserting on "code 9" specifically pins the exact failure mode we + // care about — a different code would indicate a different rejection + // path (e.g., insufficient balance, invalid payload), which would mean + // this test isn't testing what it thinks it's testing. + require.True(t, + strings.Contains(err.Error(), "code 9"), + "expected PubKeyNotRegistered rejection (error code 9 from apperrors.CodePubKeyNotRegistered); got: %v", + err, + ) + + // Step 7a: A's private balance must be UNCHANGED — rollback held. + // A single wei of deviation here = a partial-commit bug (sender debited + // despite the transfer failing). + balAPost, err := driverA.GetPrivateBalance(t.Context(), "") + require.NoError(t, err) + require.NotNil(t, balAPost, "A's post-failure balance must still be visible") + require.Equal(t, 0, oneEther.Cmp(balAPost), + "post-failure: A's balance must be UNCHANGED at 1 ETH (rollback invariant); got %s wei", balAPost.String()) + + // Step 7b: on-chain custody untouched. Private transfers don't move + // on-chain funds anyway, so this would only change if some unrelated + // bug caused the request to take a different path. + require.Equal(t, 0, oneEther.Cmp(suite.GetAppCustody(appID, ethAddr)), + "post-failure: appCustody must be UNCHANGED at 1 ETH") + + // Step 7c: B sees nothing. Unregistered anyway (so the wallet can't + // decrypt events addressed to B), but additionally the executor + // never emitted any events for this failed request. + balB, err := driverB.GetPrivateBalance(t.Context(), "") + require.NoError(t, err) + require.Nil(t, balB, + "B must have no private balance — unregistered AND the failed transfer should not have produced any event") + + // Step 7d: the stateUpdate for the failed request carries errorCode != 0 + // but is NOT a revert; the tx succeeds on-chain. The wrapper's error + // channel should therefore be empty. A non-empty channel would flag + // that the manager hit an unexpected on-chain error during this + // supposedly-clean failure path. + suite.AssertNoStateUpdateErrors(t) + + t.Logf("Private transfer to unregistered recipient failed cleanly: %v", err) +} From ba31b94d20da0b73ac982c184adfa5345f71d980 Mon Sep 17 00:00:00 2001 From: Alberto Sala Date: Fri, 24 Apr 2026 16:14:17 +0200 Subject: [PATCH 20/25] More fullstack system tests added --- ...deanonymize_unauthorized_authority_test.go | 128 ++++++++++++ .../fullstack_tests/fee_refund_claim_test.go | 165 ++++++++++++++++ .../invalid_state_root_test.go | 187 ++++++++++++++++++ 3 files changed, 480 insertions(+) create mode 100644 runtime/wasm-go/fullstack_tests/deanonymize_unauthorized_authority_test.go create mode 100644 runtime/wasm-go/fullstack_tests/fee_refund_claim_test.go create mode 100644 runtime/wasm-go/fullstack_tests/invalid_state_root_test.go diff --git a/runtime/wasm-go/fullstack_tests/deanonymize_unauthorized_authority_test.go b/runtime/wasm-go/fullstack_tests/deanonymize_unauthorized_authority_test.go new file mode 100644 index 0000000..1385272 --- /dev/null +++ b/runtime/wasm-go/fullstack_tests/deanonymize_unauthorized_authority_test.go @@ -0,0 +1,128 @@ +package main_test + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/HorizenOfficial/vela-nova/payment-app/testhelpers" + walletTestutil "github.com/HorizenOfficial/vela-nova/wallet/testutil" + "github.com/HorizenOfficial/vela/pkg/testutil/fullstack" + "github.com/stretchr/testify/require" +) + +// TestDeanonymizeRequestFromUnauthorizedAuthorityReverts proves that +// ProcessorEndpoint.submitRequest rejects a DEANONYMIZATION request from +// an account that is NOT on the application's authority allowlist. This +// is the gate that keeps user data confidential — any loosening here +// would let arbitrary accounts trigger deanonymization reports on apps +// they have no business seeing into. +// +// ## Revert path +// +// ProcessorEndpoint.sol submitRequest: +// +// } else if (requestType == Structs.RequestType.DEANONYMIZATION) { +// if (!authorityRegistry.checkAuthorityIsAllowed(applicationId, msg.sender)) { +// revert AuthorityNotAllowed(); +// } +// } +// +// The check fires at submitRequest time — before the request ever enters +// the pending queue and before any manager/executor processing. A rogue +// authority's request never costs the protocol anything beyond the gas +// spent on the (reverting) submission. +// +// ## Fullstack-unique +// +// - Hardhat unit tests cover AuthorityRegistry.checkAuthorityIsAllowed +// in isolation against a unit-test contract. +// - Only fullstack proves the wiring: that ProcessorEndpoint.submitRequest +// actually consults the registry on the DEANONYMIZATION branch, that +// msg.sender is the value compared against the allowlist, and that +// the AuthorityNotAllowed selector surfaces through the Go unpacker. +// +// ## Design +// +// - Single driver (fresh user). NewWalletDriver auto-grants DEPLOYER_ROLE +// to let it deploy the app, but does NOT grant any authority role on +// the app's AuthorityRegistry entry. The allowlist is default-empty +// for a freshly-deployed app, so this driver is unambiguously +// unauthorized as an authority. +// - No call to suite.RegisterAuthority / sim.AddAuthority for this +// driver. Skipping those is the whole point — TestDeanonymize_RealRoundTrip +// is the happy-path counterpart that DOES grant the role; this is the +// negative path that proves the gate actually fires without the grant. +// - Caller registration (RegisterUser) is NOT required: the Deanonymize +// payload is encrypted to the TEE's pubkey (read from the contract), +// not to the caller's own P521 key. The authority check fires at the +// contract level regardless of caller-side registration state. +// +// ## Flow +// +// 1. Suite start + executor + manager. +// 2. Build driver (fresh user with DEPLOYER_ROLE, no authority role). +// 3. driver.DeployApp — the ONLY reason to do this is to create an app +// whose applicationStateRoots[appID] is non-zero; without this, the +// submitRequest call would hit InvalidApplicationId first and the +// test would be testing the wrong gate. +// 4. driver.RequestReport(...) — submits a DEANONYMIZATION request from +// an unauthorized account. Contract reverts with AuthorityNotAllowed. +// The wallet driver surfaces the error. +// 5. Assert the error string contains "AuthorityNotAllowed". +// 6. suite.AssertNoStateUpdateErrors(t) — the revert is at submitRequest +// time, so the request never enters the pending queue and the manager +// never attempts a stateUpdate for it. The defensive hook must stay +// clean. +func TestDeanonymizeRequestFromUnauthorizedAuthorityReverts(t *testing.T) { + if os.Getenv("CI_FLAG") != "" { + t.Skip("Skipping fullstack test in CI environment") + } + + t.Setenv("MANAGER_ARTIFACTS_PATH", t.TempDir()) + + suite := fullstack.NewFullStackSystemTestSuite(t, "wasmtime-payment", + testhelpers.NewNetworkLogConfig(), testhelpers.NewNetworkLogConfig()) + defer suite.Cleanup() + + require.NoError(t, suite.StartExecutor()) + require.NoError(t, suite.StartManager()) + + // Step 2: fresh driver. DEPLOYER_ROLE auto-granted; authority role + // explicitly NOT granted. This is the whole asymmetry. + driver := walletTestutil.NewWalletDriver(t, suite) + + // Step 3: deploy so the app exists. Without this, submitRequest would + // hit InvalidApplicationId first (applicationStateRoots[appID] == 0), + // and the test would be testing the wrong gate. + wasmBytes := testhelpers.BuildAndLoadWasmModule(t) + wasmPath := filepath.Join(t.TempDir(), "payment_app.wasm") + require.NoError(t, os.WriteFile(wasmPath, wasmBytes, 0o644)) + + appID, err := driver.DeployApp(t.Context(), wasmPath, "100 wei", nil) + require.NoError(t, err) + require.NotZero(t, appID) + + // Step 4 + 5: unauthorized deanonymize request. The contract's + // authorityRegistry.checkAuthorityIsAllowed returns false for this + // driver's address (never granted), so submitRequest reverts with + // AuthorityNotAllowed. The wallet driver surfaces the unpacker's + // error string. + _, err = driver.RequestReport(t.Context(), "balances", "100 wei") + require.Error(t, err, "deanonymize request from unauthorized account must fail at submitRequest time") + require.True(t, + strings.Contains(err.Error(), "AuthorityNotAllowed"), + "expected AuthorityNotAllowed revert (ProcessorEndpoint's DEANONYMIZATION branch check); got: %v", err, + ) + + // Step 6: no stateUpdate was attempted — the revert rejected the + // request before it entered the pending queue, so the manager never + // processed anything for it. The wrapper's error channel must stay + // empty. A non-empty channel would indicate (a) the rejection leaked + // into the stateUpdate path somehow, or (b) some unrelated update + // hit an unexpected error during the preceding deploy. + suite.AssertNoStateUpdateErrors(t) + + t.Logf("deanonymize request from unauthorized authority correctly rejected: %v", err) +} diff --git a/runtime/wasm-go/fullstack_tests/fee_refund_claim_test.go b/runtime/wasm-go/fullstack_tests/fee_refund_claim_test.go new file mode 100644 index 0000000..901670f --- /dev/null +++ b/runtime/wasm-go/fullstack_tests/fee_refund_claim_test.go @@ -0,0 +1,165 @@ +package main_test + +import ( + "context" + "math/big" + "os" + "path/filepath" + "testing" + + "github.com/HorizenOfficial/vela-nova/payment-app/testhelpers" + walletTestutil "github.com/HorizenOfficial/vela-nova/wallet/testutil" + "github.com/HorizenOfficial/vela/pkg/testutil/fullstack" + ethCommon "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +// TestFeeRefundClaim proves the fee-refund mechanism works end-to-end: +// users who submit maxFeeValue above the executor's actual applicationFee +// get the difference credited to pendingClaims[ETH][user] and can claim +// it. A regression anywhere along this chain — the executor's fee +// computation, the contract's pendingClaims accumulator, the claim +// payout — would silently cost users money on every request. +// +// ## What's being exercised +// +// Three layers cooperate: +// +// 1. Executor-side fee computation (pkg/executor/executor.go): +// applicationFee = max(fuel × FuelPricePerUnit, MinFeePerRequest) +// refundAmount = maxFeeValue − applicationFee +// Both values go into the signed UpdatePayload. +// +// 2. Contract-side accumulator (ProcessorEndpoint.sol, the success branch +// of stateUpdate): +// if (refund > 0) _asyncTransfer(ETH_TOKEN, feeRecipient, refund); +// → pendingClaims[ETH][user] += refund on each successful request. +// +// 3. Claim payout (ProcessorEndpoint.claim): +// transfers pendingClaims[token][payee] to payee on-chain and +// zeroes the accumulator. +// +// ## Fullstack-unique +// +// - Mocks stub the fee accounting entirely — no applicationFee +// computation, no pendingClaims accumulator, no claim flow. +// - Only fullstack exercises the full chain: real fuel costs inside a +// real Wasmtime runtime → real executor arithmetic → real on-chain +// UpdatePayload fields → real contract-side accumulator → real +// on-chain payout. +// +// ## Claim-path choice +// +// Two ways to drive the claim: +// +// - driver.ClaimPendingPayments(ctx, "") — the wallet CLI path. Fully +// realistic but the user pays gas in ETH, so the user's balance delta +// is (refund − gasCost). Asserting this exactly requires extracting +// the tx receipt from the driver, which the current wrapper doesn't +// expose. +// - sim.Claim(ETH_TOKEN, user) — the contract's claim is public; anyone +// can call it on the user's behalf and the funds still go to the +// payee. The deployer account pays gas here, so the user's balance +// delta is exactly the refund. +// +// We use sim.Claim for the exact-wei balance assertion. The wallet's +// ClaimPendingPayments command is already exercised by TestERC20FullStack +// (for MOCK tokens); this test's job is to pin the fee-accumulator +// invariant to the wei, and the clean math makes that possible. +// +// ## Flow +// +// 1. Suite start + executor + manager. Build driver. +// 2. Three happy-path requests (deploy + register + deposit), each with +// maxFeeValue = 100 wei. Each successful stateUpdate credits some +// refund to pendingClaims[ETH][driver]. Three requests is enough to +// make the accumulator unambiguously non-trivial. +// 3. Capture refundTotal = sim.GetPendingClaims(ETH, user) — must be > 0. +// 4. Capture balanceBefore = BalanceAt(user). +// 5. sim.Claim(ETH_TOKEN, user) — deployer pays gas, user receives funds. +// 6. Assertions: +// a. balanceAfter == balanceBefore + refundTotal (exact; no gas on user side) +// b. pendingClaims[ETH][user] == 0 (accumulator cleared) +// 7. AssertNoStateUpdateErrors — pure happy-path flow; the wrapper's +// error channel should be empty. +func TestFeeRefundClaim(t *testing.T) { + if os.Getenv("CI_FLAG") != "" { + t.Skip("Skipping fullstack test in CI environment") + } + + t.Setenv("MANAGER_ARTIFACTS_PATH", t.TempDir()) + + suite := fullstack.NewFullStackSystemTestSuite(t, "wasmtime-payment", + testhelpers.NewNetworkLogConfig(), testhelpers.NewNetworkLogConfig()) + defer suite.Cleanup() + + require.NoError(t, suite.StartExecutor()) + require.NoError(t, suite.StartManager()) + + // Step 2: three happy-path requests. Each carries maxFeeValue = 100 wei, + // which is well above the executor's MinFeePerRequest (10 wei default), + // so each produces a non-trivial refund. + driver := walletTestutil.NewWalletDriver(t, suite) + user := driver.UserAddress() + + wasmBytes := testhelpers.BuildAndLoadWasmModule(t) + wasmPath := filepath.Join(t.TempDir(), "payment_app.wasm") + require.NoError(t, os.WriteFile(wasmPath, wasmBytes, 0o644)) + + appID, err := driver.DeployApp(t.Context(), wasmPath, "100 wei", nil) + require.NoError(t, err) + require.NotZero(t, appID) + require.NoError(t, driver.RegisterUser(t.Context(), "100 wei")) + require.NoError(t, driver.Deposit(t.Context(), "1 ETH", "", "100 wei")) + + // Step 3: refunds accumulated across the three requests. If this is 0, + // the accumulator is broken somewhere between the executor computing + // refundAmount and the contract crediting pendingClaims. + sim := suite.GetSimTestHelper() + ethAddr := ethCommon.Address{} + refundTotal := sim.GetPendingClaims(ethAddr, user) + require.Equal(t, 1, refundTotal.Sign(), + "pendingClaims[ETH][user] should be strictly positive after three successful requests with maxFee=100 wei; got %s", + refundTotal.String()) + + // Step 4: snapshot the user's on-chain balance before the claim. After + // the claim we expect this to increase by exactly refundTotal (deployer + // pays the gas in step 5). + ctx := context.Background() + balanceBefore, err := sim.Client().BalanceAt(ctx, user, nil) + require.NoError(t, err) + + // Step 5: payout via the contract's public claim. Deployer account is + // the msg.sender here (SimTestHelper.Claim wires that up); the payee + // argument is the user, so funds land in user's on-chain balance + // without any gas deduction from the user. + sim.WaitMined(sim.Claim(ethAddr, user)) + + // Step 6a: exact balance equality. If the contract paid out a different + // amount than the accumulator said, this would fail. If the wrong + // account received the funds, the user's balance wouldn't move at all. + balanceAfter, err := sim.Client().BalanceAt(ctx, user, nil) + require.NoError(t, err) + expectedBalanceAfter := new(big.Int).Add(balanceBefore, refundTotal) + require.Equal(t, 0, expectedBalanceAfter.Cmp(balanceAfter), + "post-claim user ETH balance should increase by exactly refundTotal (%s wei); "+ + "before=%s, after=%s, delta=%s", + refundTotal.String(), + balanceBefore.String(), + balanceAfter.String(), + new(big.Int).Sub(balanceAfter, balanceBefore).String(), + ) + + // Step 6b: accumulator cleared. If this is still non-zero, the contract's + // claim didn't zero the pendingClaims entry — would lead to double-claim + // in the wild. + require.Equal(t, 0, big.NewInt(0).Cmp(sim.GetPendingClaims(ethAddr, user)), + "pendingClaims[ETH][user] should be zero after claim") + + // Step 7: pure happy-path test; nothing should have reverted on the + // manager's stateUpdate path. A non-empty buffer would indicate an + // unrelated regression interfering with the fee flow. + suite.AssertNoStateUpdateErrors(t) + + t.Logf("Fee refund claim verified: accumulated %s wei across 3 requests, claimed cleanly", refundTotal.String()) +} diff --git a/runtime/wasm-go/fullstack_tests/invalid_state_root_test.go b/runtime/wasm-go/fullstack_tests/invalid_state_root_test.go new file mode 100644 index 0000000..f93094c --- /dev/null +++ b/runtime/wasm-go/fullstack_tests/invalid_state_root_test.go @@ -0,0 +1,187 @@ +package main_test + +import ( + "math/big" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/HorizenOfficial/vela-nova/payment-app/testhelpers" + walletTestutil "github.com/HorizenOfficial/vela-nova/wallet/testutil" + "github.com/HorizenOfficial/vela/pkg/common" + "github.com/HorizenOfficial/vela/pkg/testutil/fullstack" + ethCommon "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +// TestInvalidPrevStateRootRejection proves that ProcessorEndpoint.stateUpdate +// rejects an update whose prevStateRoot does not match the contract's +// stored root for the target app. This guard is what prevents: +// +// - Reorg-induced state drift: if the chain reorged past the manager's +// latest known state, resubmitting a stale update would silently +// overwrite newer on-chain state. +// - Replay of a stale stateUpdate by any UPDATE_STATUS_ROLE-holder. +// - Divergent concurrent managers submitting incompatible updates. +// +// Fullstack-unique: +// - Hardhat tests can exercise the prevStateRoot comparison in isolation +// against a unit-test contract instance. +// - Only fullstack proves the wiring: that the check sits on the real +// stateUpdate path, fires in the right order (after role, requestID +// and appID checks, BEFORE the TEE signature check), and surfaces the +// InvalidStateRoot selector through UnpackProcessorEndpointError. +// +// ## Attack path +// +// The contract's stateUpdate performs guards in this order (see +// ProcessorEndpoint.sol): +// +// 1. onlyRole(UPDATE_STATUS_ROLE) — S3 tests this. +// 2. isCurrentPendingRequest(processedRequestId) — InvalidRequestId. +// 3. applicationId == requestInfo.applicationId — InvalidApplicationId. +// 4. prevStateRoot == applicationStateRoots[appId] — InvalidStateRoot (THIS test). +// 5. teeAuthenticator.checkSignature(...) — S2 tests this. +// +// To trigger InvalidStateRoot specifically we need to pass guards 1-3 and +// fail guard 4: +// +// - Sign with the manager's key (guard 1). Done via SubmitStateUpdateAsManager. +// - Use a real pending request ID (guard 2). A freshly-submitted request +// that the manager has not yet processed is the natural fit. +// - Use the matching application ID (guard 3). +// - Use a stale prev-state-root: zero bytes is the unambiguous "wrong" +// value once the app has been deployed, since deploy + register + +// deposit have advanced the stored root to a non-zero value. +// +// ## Flow +// +// 1. Start suite + executor + manager. Build driver. Deploy the app, +// register the user, deposit 1 ETH — three happy-path state +// transitions. applicationStateRoots[appID] is now unambiguously a +// non-zero value V. +// 2. suite.StopManager() — we are about to submit a stateUpdate as the +// manager ourselves; stopping the manager prevents nonce contention +// between its polling loop and our direct submission. +// 3. Submit a fresh deposit request DIRECTLY via SimTestHelper (no +// driver wrapper — we don't want to block waiting for completion, +// since the stopped manager will never complete it). Extract the +// requestID from the RequestSubmitted event. This satisfies guard 2. +// 4. Craft a stateUpdate payload pairing that real requestID with a +// DELIBERATELY stale prev-state-root ([32]byte{}). Since the app's +// real stored root is non-zero, guard 4 must reject. +// 5. suite.SubmitStateUpdateAsManager(payload) → expect the Go unpacker +// to surface "InvalidStateRoot". +// 6. Sanity cross-check: AssertNoStateUpdateErrors — the +// SubmitStateUpdateAsManager path builds a fresh client and bypasses +// the wrapped eventBroadcastingClient, so its error channel must be +// empty. A non-empty channel would indicate the bypass is broken or +// the (stopped) manager somehow leaked an error into the wrapper. +func TestInvalidPrevStateRootRejection(t *testing.T) { + if os.Getenv("CI_FLAG") != "" { + t.Skip("Skipping fullstack test in CI environment") + } + + t.Setenv("MANAGER_ARTIFACTS_PATH", t.TempDir()) + + suite := fullstack.NewFullStackSystemTestSuite(t, "wasmtime-payment", + testhelpers.NewNetworkLogConfig(), testhelpers.NewNetworkLogConfig()) + defer suite.Cleanup() + + require.NoError(t, suite.StartExecutor()) + require.NoError(t, suite.StartManager()) + + // Step 1: three happy-path stateUpdates move applicationStateRoots[appID] + // from zero to some non-zero value V. Exact value doesn't matter — we + // only need "non-zero" so that [32]byte{} is unambiguously wrong. + driver := walletTestutil.NewWalletDriver(t, suite) + + wasmBytes := testhelpers.BuildAndLoadWasmModule(t) + wasmPath := filepath.Join(t.TempDir(), "payment_app.wasm") + require.NoError(t, os.WriteFile(wasmPath, wasmBytes, 0o644)) + + appID, err := driver.DeployApp(t.Context(), wasmPath, "100 wei", nil) + require.NoError(t, err) + require.NotZero(t, appID) + require.NoError(t, driver.RegisterUser(t.Context(), "100 wei")) + require.NoError(t, driver.Deposit(t.Context(), "1 ETH", "", "100 wei")) + + // Step 2: stop the running manager. After this, no stateUpdates happen + // via the manager's polling loop — the test owns the manager-signed + // submission path. + require.NoError(t, suite.StopManager()) + + // Step 3: submit a fresh deposit request directly on-chain. Using + // SimTestHelper.SubmitRequestFromUser bypasses the wallet driver's + // wait-for-completion logic; we need the request to sit in the + // pending queue unprocessed so it satisfies guard 2 when our bogus + // stateUpdate references it. + sim := suite.GetSimTestHelper() + user := driver.UserAddress() + userOpts, err := suite.GetTransactOpts(user) + require.NoError(t, err) + + oneTenthEth := new(big.Int).Exp(big.NewInt(10), big.NewInt(17), nil) // 0.1 ETH + maxFee := big.NewInt(100) // 100 wei + depositTx := sim.SubmitRequestFromUser( + appID, + common.Process, + nil, // empty payload — guard 2 doesn't inspect it + ethCommon.Address{}, + oneTenthEth, + maxFee, + userOpts, + ) + sim.WaitMined(depositTx) + requestID := sim.GetRequestSubmittedEvent(depositTx).RequestId + + // Step 4: craft the bogus payload. Zero prev-root is the unambiguous + // "obviously wrong" choice once the app has been deployed. ErrorCode, + // empty signature, etc. are irrelevant — guard 4 fires before guard 5 + // reads them. + bogus := &common.UpdatePayload{ + ApplicationID: appID, + RequestID: requestID, + PrevStateRoot: [32]byte{}, // deliberately stale: real stored root is non-zero + NewStateRoot: [32]byte{0xAA}, // arbitrary + Events: []common.Event{}, + AppEvents: []common.AppEvent{}, + Withdrawals: []common.Withdrawal{}, + Signature: []byte{}, // guard 5 would reject, but guard 4 fires first + RefundAmount: common.NewBig(0), + ApplicationFee: common.NewBig(0), + ErrorCode: 0, + ErrorMsg: "", + } + + // Step 5: submit as the manager (guard 1 passes). Guards 2 and 3 pass + // because we used a real pending requestID for the matching appID. + // Guard 4 fails because prev=zero ≠ stored non-zero root. + err = suite.SubmitStateUpdateAsManager(bogus) + require.Error(t, err, "stateUpdate with stale prevStateRoot must be rejected") + require.True(t, + strings.Contains(err.Error(), "InvalidStateRoot"), + "expected InvalidStateRoot revert (ProcessorEndpoint guard 4); got: %v", err, + ) + + // Step 6: the bypass path (fresh client via SubmitStateUpdateAsManager) + // must not leak errors into the wrapped eventBroadcastingClient's + // buffer. A non-empty buffer would indicate either (a) the bypass was + // inadvertently re-routed through the wrapper, or (b) the stopped + // manager somehow emitted an erroring update before we took over — + // both would invalidate the isolation this test relies on. + suite.AssertNoStateUpdateErrors(t) + + // Belt-and-braces: the failed stateUpdate must have reverted the whole + // tx, so applicationStateRoots[appID] must still equal the happy-path + // value V. We don't know V's exact value but we know it's NOT zero + // and NOT our bogus 0xAA root. + stored := sim.GetStateRoot(appID) + require.NotEqual(t, [32]byte{}, stored, + "after failed stateUpdate, stored root must remain non-zero (happy-path value V)") + require.NotEqual(t, [32]byte{0xAA}, stored, + "after failed stateUpdate, stored root must NOT be our bogus newStateRoot (rollback held)") + + t.Logf("stateUpdate with stale prevStateRoot correctly rejected: %v", err) +} From 5187990b708c21dd9a151cc54904553f058f2f73 Mon Sep 17 00:00:00 2001 From: Alberto Sala Date: Tue, 28 Apr 2026 17:06:01 +0200 Subject: [PATCH 21/25] Using vela-common-go definition for native-token constant --- .../fullstack_tests/fee_refund_claim_test.go | 4 ++-- .../fullstack_tests/full_stack_restart_test.go | 4 ++-- .../fullstack_tests/invalid_state_root_test.go | 4 ++-- .../fullstack_tests/multi_app_isolation_test.go | 4 ++-- .../fullstack_tests/payment_app_fullstack_test.go | 2 +- .../private_transfer_unregistered_test.go | 4 ++-- runtime/wasm-go/go.mod | 2 +- runtime/wasm-go/integration_test.go | 5 +++-- .../wasm-go/system_tests/payment_app_system_test.go | 13 +++++++------ runtime/wasm-go/wasmtime_runtime_test.go | 2 +- wallet/app/save_config_test.go | 4 ++-- wallet/app/tokens.go | 7 ++++--- wallet/app/tokens_test.go | 5 +++-- wallet/app/types.go | 3 ++- wallet/cmd/constants.go | 8 ++++---- wallet/cmd/deployapp.go | 3 +-- wallet/go.mod | 2 +- wallet/testutil/wallet_driver.go | 3 ++- 18 files changed, 42 insertions(+), 37 deletions(-) diff --git a/runtime/wasm-go/fullstack_tests/fee_refund_claim_test.go b/runtime/wasm-go/fullstack_tests/fee_refund_claim_test.go index 901670f..92c2d9a 100644 --- a/runtime/wasm-go/fullstack_tests/fee_refund_claim_test.go +++ b/runtime/wasm-go/fullstack_tests/fee_refund_claim_test.go @@ -7,10 +7,10 @@ import ( "path/filepath" "testing" + velacommon "github.com/HorizenOfficial/vela-common-go/common" "github.com/HorizenOfficial/vela-nova/payment-app/testhelpers" walletTestutil "github.com/HorizenOfficial/vela-nova/wallet/testutil" "github.com/HorizenOfficial/vela/pkg/testutil/fullstack" - ethCommon "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" ) @@ -116,7 +116,7 @@ func TestFeeRefundClaim(t *testing.T) { // the accumulator is broken somewhere between the executor computing // refundAmount and the contract crediting pendingClaims. sim := suite.GetSimTestHelper() - ethAddr := ethCommon.Address{} + ethAddr := velacommon.ETH_TOKEN refundTotal := sim.GetPendingClaims(ethAddr, user) require.Equal(t, 1, refundTotal.Sign(), "pendingClaims[ETH][user] should be strictly positive after three successful requests with maxFee=100 wei; got %s", diff --git a/runtime/wasm-go/fullstack_tests/full_stack_restart_test.go b/runtime/wasm-go/fullstack_tests/full_stack_restart_test.go index 64fdbd1..9a27dd7 100644 --- a/runtime/wasm-go/fullstack_tests/full_stack_restart_test.go +++ b/runtime/wasm-go/fullstack_tests/full_stack_restart_test.go @@ -6,10 +6,10 @@ import ( "path/filepath" "testing" + velacommon "github.com/HorizenOfficial/vela-common-go/common" "github.com/HorizenOfficial/vela-nova/payment-app/testhelpers" walletTestutil "github.com/HorizenOfficial/vela-nova/wallet/testutil" "github.com/HorizenOfficial/vela/pkg/testutil/fullstack" - ethCommon "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" ) @@ -96,7 +96,7 @@ func TestFullStackRestart_KeysetRecovery(t *testing.T) { require.NotZero(t, appID) require.NoError(t, driver.RegisterUser(t.Context(), "100 wei")) - ethAddr := ethCommon.Address{} + ethAddr := velacommon.ETH_TOKEN oneEther := new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil) halfEther := new(big.Int).Div(oneEther, big.NewInt(2)) threeTenthsEther := new(big.Int).Mul(big.NewInt(3), new(big.Int).Exp(big.NewInt(10), big.NewInt(17), nil)) // 0.3 ETH diff --git a/runtime/wasm-go/fullstack_tests/invalid_state_root_test.go b/runtime/wasm-go/fullstack_tests/invalid_state_root_test.go index f93094c..7d3d3e6 100644 --- a/runtime/wasm-go/fullstack_tests/invalid_state_root_test.go +++ b/runtime/wasm-go/fullstack_tests/invalid_state_root_test.go @@ -7,11 +7,11 @@ import ( "strings" "testing" + velacommon "github.com/HorizenOfficial/vela-common-go/common" "github.com/HorizenOfficial/vela-nova/payment-app/testhelpers" walletTestutil "github.com/HorizenOfficial/vela-nova/wallet/testutil" "github.com/HorizenOfficial/vela/pkg/common" "github.com/HorizenOfficial/vela/pkg/testutil/fullstack" - ethCommon "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" ) @@ -128,7 +128,7 @@ func TestInvalidPrevStateRootRejection(t *testing.T) { appID, common.Process, nil, // empty payload — guard 2 doesn't inspect it - ethCommon.Address{}, + velacommon.ETH_TOKEN, oneTenthEth, maxFee, userOpts, diff --git a/runtime/wasm-go/fullstack_tests/multi_app_isolation_test.go b/runtime/wasm-go/fullstack_tests/multi_app_isolation_test.go index c8385ff..cde2e91 100644 --- a/runtime/wasm-go/fullstack_tests/multi_app_isolation_test.go +++ b/runtime/wasm-go/fullstack_tests/multi_app_isolation_test.go @@ -6,10 +6,10 @@ import ( "path/filepath" "testing" + velacommon "github.com/HorizenOfficial/vela-common-go/common" "github.com/HorizenOfficial/vela-nova/payment-app/testhelpers" walletTestutil "github.com/HorizenOfficial/vela-nova/wallet/testutil" "github.com/HorizenOfficial/vela/pkg/testutil/fullstack" - ethCommon "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" ) @@ -129,7 +129,7 @@ func TestMultiAppIsolation(t *testing.T) { require.NoError(t, driver.RegisterUser(t.Context(), "100 wei")) // Handy constants reused across the assertions. - ethAddr := ethCommon.Address{} // sentinel for native ETH in appCustody + ethAddr := velacommon.ETH_TOKEN // sentinel for native ETH in appCustody zero := big.NewInt(0) oneEther := new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil) halfEther := new(big.Int).Div(oneEther, big.NewInt(2)) diff --git a/runtime/wasm-go/fullstack_tests/payment_app_fullstack_test.go b/runtime/wasm-go/fullstack_tests/payment_app_fullstack_test.go index e7941a1..9b43449 100644 --- a/runtime/wasm-go/fullstack_tests/payment_app_fullstack_test.go +++ b/runtime/wasm-go/fullstack_tests/payment_app_fullstack_test.go @@ -91,7 +91,7 @@ func TestFullStackDeployAndDeposit(t *testing.T) { // Deposit 2 ETH depositAmount := big.NewInt(2_000_000_000_000_000_000) - depositReq, err := cryptoHelper.CreateTokenDepositRequest(appID, commontestutil.GenerateRandomRequestID(), userAddress, ethCommon.Address{}, depositAmount, executorPubKey) + depositReq, err := cryptoHelper.CreateTokenDepositRequest(appID, commontestutil.GenerateRandomRequestID(), userAddress, velacommon.ETH_TOKEN, depositAmount, executorPubKey) require.NoError(t, err) require.NoError(t, suite.SubmitRequest(depositReq)) require.NoError(t, suite.AssertRequestCompleted(depositReq.RequestID, timeout)) diff --git a/runtime/wasm-go/fullstack_tests/private_transfer_unregistered_test.go b/runtime/wasm-go/fullstack_tests/private_transfer_unregistered_test.go index a028fcf..0668d18 100644 --- a/runtime/wasm-go/fullstack_tests/private_transfer_unregistered_test.go +++ b/runtime/wasm-go/fullstack_tests/private_transfer_unregistered_test.go @@ -7,10 +7,10 @@ import ( "strings" "testing" + velacommon "github.com/HorizenOfficial/vela-common-go/common" "github.com/HorizenOfficial/vela-nova/payment-app/testhelpers" walletTestutil "github.com/HorizenOfficial/vela-nova/wallet/testutil" "github.com/HorizenOfficial/vela/pkg/testutil/fullstack" - ethCommon "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" ) @@ -115,7 +115,7 @@ func TestPrivateTransferToUnregisteredRecipient(t *testing.T) { // deliberately NOT: driverB.RegisterUser(...) // Step 5: establish the pre-condition — A has funds to attempt to send. - ethAddr := ethCommon.Address{} + ethAddr := velacommon.ETH_TOKEN oneEther := new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil) require.NoError(t, driverA.Deposit(t.Context(), "1 ETH", "", "100 wei")) diff --git a/runtime/wasm-go/go.mod b/runtime/wasm-go/go.mod index d028e4f..39b4b12 100644 --- a/runtime/wasm-go/go.mod +++ b/runtime/wasm-go/go.mod @@ -9,7 +9,7 @@ toolchain go1.24.3 // Can be useful for local developments replace github.com/HorizenOfficial/vela => ../../../vela -//replace github.com/HorizenOfficial/vela-common-go => ../../../vela-common-go +replace github.com/HorizenOfficial/vela-common-go => ../../../vela-common-go replace github.com/HorizenOfficial/vela-nova/wallet => ../../wallet diff --git a/runtime/wasm-go/integration_test.go b/runtime/wasm-go/integration_test.go index ee9e1b0..424cd4b 100644 --- a/runtime/wasm-go/integration_test.go +++ b/runtime/wasm-go/integration_test.go @@ -12,6 +12,7 @@ import ( "strings" + velacommon "github.com/HorizenOfficial/vela-common-go/common" "github.com/HorizenOfficial/vela-common-go/wasm/types" "golang.org/x/crypto/sha3" "github.com/HorizenOfficial/vela-nova/payment-app/app" @@ -24,7 +25,7 @@ import ( ) var ( - ethToken = ethCommon.Address{} // zero address = ETH + ethToken = velacommon.ETH_TOKEN // canonical native-token sentinel usdcToken = ethCommon.HexToAddress("0xcafeBABE00000000000000000000000000000001") // synthetic test-only USDC address usdcTokenHex = strings.ToLower(usdcToken.Hex()) // lowercase to match wasm types.Address.Hex() ) @@ -401,7 +402,7 @@ func TestIntegration_ProcessRequest_Withdrawal(t *testing.T) { require.Len(t, withdrawals, 1) assert.Equal(t, ethWithdrawAddr, withdrawals[0].DestinationAddress) assert.Equal(t, withdrawValue.String(), withdrawals[0].Amount.String()) - assert.Equal(t, ethCommon.Address{}, withdrawals[0].TokenAddress, "ETH withdrawal should have zero token address") + assert.Equal(t, ethToken, withdrawals[0].TokenAddress, "ETH withdrawal should have zero token address") require.Equal(t, 0, fuel.Cmp(big.NewInt(50))) expectedBalance := types.NewUint256(0) diff --git a/runtime/wasm-go/system_tests/payment_app_system_test.go b/runtime/wasm-go/system_tests/payment_app_system_test.go index bdff340..50e7c24 100644 --- a/runtime/wasm-go/system_tests/payment_app_system_test.go +++ b/runtime/wasm-go/system_tests/payment_app_system_test.go @@ -12,6 +12,7 @@ import ( "testing" "time" + velacommon "github.com/HorizenOfficial/vela-common-go/common" "github.com/HorizenOfficial/vela-common-go/wasm/types" "github.com/HorizenOfficial/vela-nova/payment-app/app" "github.com/HorizenOfficial/vela-nova/payment-app/testhelpers" @@ -257,7 +258,7 @@ func registerNoSeedUser(t *testing.T, suite *systemTests.SystemTestSuite, crypto Sender: user, Timestamp: common.ToBig(new(big.Int).SetInt64(time.Now().Unix())), AssetAmount: common.NewBig(0), - TokenAddress: ethCommon.Address{}, + TokenAddress: velacommon.ETH_TOKEN, MaxFeeValue: common.NewBig(100), } require.NoError(t, suite.SubmitRequest(req)) @@ -362,11 +363,11 @@ func TestPaymentAppFullFlow(t *testing.T) { // Deposit 2 ETH and validate event fields (seed-registered user -> hashed subtypes) depositAmount := big.NewInt(2000000000000000000) - depositToPaymentApp(t, suite, cryptoHelper, appID, commontestutil.GenerateRandomRequestID(), userAddress, ethCommon.Address{}, depositAmount, true) + depositToPaymentApp(t, suite, cryptoHelper, appID, commontestutil.GenerateRandomRequestID(), userAddress, velacommon.ETH_TOKEN, depositAmount, true) // Withdraw 0.5 ETH and validate event fields withdrawAmount := big.NewInt(500000000000000000) - withdrawFromPaymentApp(t, suite, cryptoHelper, appID, commontestutil.GenerateRandomRequestID(), userAddress, recipientAddress, ethCommon.Address{}, withdrawAmount) + withdrawFromPaymentApp(t, suite, cryptoHelper, appID, commontestutil.GenerateRandomRequestID(), userAddress, recipientAddress, velacommon.ETH_TOKEN, withdrawAmount) // --- No-seed user: register without a seed, deposit, and verify that // WaitForEvent with the plaintext "deposit" subtype works. When no seed @@ -377,14 +378,14 @@ func TestPaymentAppFullFlow(t *testing.T) { // Deposit 1 ETH for the no-seed user; plaintext "deposit" subtype should match noSeedDepositAmount := big.NewInt(1000000000000000000) - depositToPaymentApp(t, suite, cryptoHelper, appID, commontestutil.GenerateRandomRequestID(), noSeedUser, ethCommon.Address{}, noSeedDepositAmount, false) + depositToPaymentApp(t, suite, cryptoHelper, appID, commontestutil.GenerateRandomRequestID(), noSeedUser, velacommon.ETH_TOKEN, noSeedDepositAmount, false) // Deanonymization report as auditor — verifies final state after deposit and withdrawal. // Verify balances: seed user (2 ETH - 0.5 ETH = 1.5 ETH) and no-seed user (1 ETH) accounts := fetchDeanonAccounts(t, suite, cryptoHelper, executorPubKey, appID, auditorAddress, timeout) require.Len(t, accounts, 2, "expected two accounts in report (seed user + no-seed user)") - ethTokenHex := ethCommon.Address{}.Hex() + ethTokenHex := velacommon.ETH_TOKEN.Hex() expectedBalances := map[ethCommon.Address]*big.Int{ userAddress: new(big.Int).Sub(depositAmount, withdrawAmount), // 1.5 ETH noSeedUser: noSeedDepositAmount, // 1 ETH @@ -471,7 +472,7 @@ func TestPaymentAppERC20FullFlow(t *testing.T) { // containing tokenAddress, the deposit below would be rejected by the // guest and AssertRequestCompleted would fail. The decrypted deanonymization // report at the end also shows the balance keyed under tokenAddress. - ethHex := strings.ToLower(ethCommon.Address{}.Hex()) + ethHex := strings.ToLower(velacommon.ETH_TOKEN.Hex()) tokenHex := strings.ToLower(tokenAddress.Hex()) // --- Register user and auditor keys --- diff --git a/runtime/wasm-go/wasmtime_runtime_test.go b/runtime/wasm-go/wasmtime_runtime_test.go index 209c9b3..3f19cf6 100644 --- a/runtime/wasm-go/wasmtime_runtime_test.go +++ b/runtime/wasm-go/wasmtime_runtime_test.go @@ -349,7 +349,7 @@ func TestWasmtimeRuntime_ProcessRequest_Withdrawal(t *testing.T) { withdrawal := withdrawals[0] assert.Equal(t, withdrawAddress, withdrawal.DestinationAddress) assert.Equal(t, withdrawValue, withdrawal.Amount.ToInt()) - assert.Equal(t, ethCommon.Address{}, withdrawal.TokenAddress, "ETH withdrawal should have zero token address") + assert.Equal(t, ethToken, withdrawal.TokenAddress, "ETH withdrawal should have zero token address") // Verify the state was updated var stateData applicationInternalState diff --git a/wallet/app/save_config_test.go b/wallet/app/save_config_test.go index de8cb5a..8946df6 100644 --- a/wallet/app/save_config_test.go +++ b/wallet/app/save_config_test.go @@ -6,9 +6,9 @@ import ( "strings" "testing" + velacommon "github.com/HorizenOfficial/vela-common-go/common" "github.com/HorizenOfficial/vela/pkg/common" "github.com/HorizenOfficial/vela/pkg/crypto" - ethCommon "github.com/ethereum/go-ethereum/common" "github.com/magiconair/properties" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -115,7 +115,7 @@ func TestSaveConfigToFile_SkipsImplicitETH(t *testing.T) { content := string(data) assert.NotContains(t, content, "token.ETH.") - assert.NotContains(t, content, ethCommon.Address{}.Hex()) + assert.NotContains(t, content, velacommon.ETH_TOKEN.Hex()) } // TestLoadConfigFromFile_RejectsMalformedTokens verifies that corrupt token diff --git a/wallet/app/tokens.go b/wallet/app/tokens.go index 28ffc31..830301f 100644 --- a/wallet/app/tokens.go +++ b/wallet/app/tokens.go @@ -6,6 +6,7 @@ import ( "strconv" "strings" + velacommon "github.com/HorizenOfficial/vela-common-go/common" ethCommon "github.com/ethereum/go-ethereum/common" "github.com/magiconair/properties" ) @@ -20,7 +21,7 @@ type TokenInfo struct { // ethTokenInfo is the implicit ETH entry — always available, never in config. var ethTokenInfo = TokenInfo{ Symbol: "ETH", - Address: ethCommon.Address{}, + Address: velacommon.ETH_TOKEN, Decimals: 18, } @@ -75,7 +76,7 @@ func LoadTokenRegistry(config *properties.Properties) (*TokenRegistry, error) { return nil, fmt.Errorf("token %s: invalid address %q", sym, addrStr) } addr := ethCommon.HexToAddress(addrStr) - if addr == (ethCommon.Address{}) { + if addr == velacommon.ETH_TOKEN { return nil, fmt.Errorf("token %s: zero address is reserved for ETH", sym) } @@ -145,7 +146,7 @@ func (r *TokenRegistry) ResolveToken(symbolOrAddress string) (*TokenInfo, error) func (r *TokenRegistry) AllTokenAddresses() []string { var addrs []string for _, t := range r.bySymbol { - if t.Address != (ethCommon.Address{}) { + if t.Address != velacommon.ETH_TOKEN { addrs = append(addrs, strings.ToLower(t.Address.Hex())) } } diff --git a/wallet/app/tokens_test.go b/wallet/app/tokens_test.go index 317580a..1a14100 100644 --- a/wallet/app/tokens_test.go +++ b/wallet/app/tokens_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" + velacommon "github.com/HorizenOfficial/vela-common-go/common" ethCommon "github.com/ethereum/go-ethereum/common" "github.com/magiconair/properties" "github.com/stretchr/testify/assert" @@ -36,7 +37,7 @@ func TestLoadTokenRegistry_NilConfig_OnlyETH(t *testing.T) { require.NoError(t, err) require.Equal(t, "ETH", eth.Symbol) require.Equal(t, uint8(18), eth.Decimals) - require.Equal(t, ethCommon.Address{}, eth.Address) + require.Equal(t, velacommon.ETH_TOKEN, eth.Address) // No ERC-20 tokens are registered. require.Empty(t, r.AllTokenAddresses()) @@ -294,7 +295,7 @@ func TestAllTokenAddresses_ExcludesETHAndReturnsLowercase(t *testing.T) { require.Len(t, addrs, 1) // Lowercased, not checksummed. assert.Equal(t, strings.ToLower(usdcAddrHex), addrs[0]) - assert.NotContains(t, addrs, ethCommon.Address{}.Hex()) + assert.NotContains(t, addrs, velacommon.ETH_TOKEN.Hex()) } // ---------- FormatTokenAmount ---------- diff --git a/wallet/app/types.go b/wallet/app/types.go index 57a26c4..777ee66 100644 --- a/wallet/app/types.go +++ b/wallet/app/types.go @@ -11,6 +11,7 @@ import ( "strings" "time" + velacommon "github.com/HorizenOfficial/vela-common-go/common" "github.com/HorizenOfficial/vela-common-go/subgraph" "github.com/HorizenOfficial/vela/pkg/blockchain" "github.com/HorizenOfficial/vela/pkg/common" @@ -199,7 +200,7 @@ func SaveConfigToFile(cfg *Config, path string) error { // Serialize token registry entries if cfg.Tokens != nil { for _, t := range cfg.Tokens.bySymbol { - if t.Address != (ethCommon.Address{}) { // skip ETH (implicit) + if t.Address != velacommon.ETH_TOKEN { // skip ETH (implicit) sym := strings.ToUpper(t.Symbol) lines = append(lines, fmt.Sprintf("token.%s.address=%s", sym, t.Address.Hex())) lines = append(lines, fmt.Sprintf("token.%s.decimals=%d", sym, t.Decimals)) diff --git a/wallet/cmd/constants.go b/wallet/cmd/constants.go index b6ff6aa..66b6e4c 100644 --- a/wallet/cmd/constants.go +++ b/wallet/cmd/constants.go @@ -4,14 +4,14 @@ import ( "context" "math/big" + velacommon "github.com/HorizenOfficial/vela-common-go/common" "github.com/HorizenOfficial/vela-nova/wallet/app" - ethCommon "github.com/ethereum/go-ethereum/common" "github.com/spf13/cobra" ) -// ETH_TOKEN is the sentinel address representing native ETH. -// Matches Structs.sol: address constant ETH_TOKEN = address(0). -var ETH_TOKEN = ethCommon.Address{} +// ETH_TOKEN is the package-local alias for the canonical native-token sentinel +// in vela-common-go. Matches Structs.sol: address constant ETH_TOKEN = address(0). +var ETH_TOKEN = velacommon.ETH_TOKEN const PROTOCOL_VERSION uint8 = 0 const BLOCK_BATCH_SIZE = 100000 diff --git a/wallet/cmd/deployapp.go b/wallet/cmd/deployapp.go index 9eb4bf7..2708a6c 100644 --- a/wallet/cmd/deployapp.go +++ b/wallet/cmd/deployapp.go @@ -18,7 +18,6 @@ import ( runtimeapp "github.com/HorizenOfficial/vela-nova/payment-app/app" "github.com/HorizenOfficial/vela-nova/wallet/app" "github.com/HorizenOfficial/vela/pkg/blockchain" - ethCommon "github.com/ethereum/go-ethereum/common" "github.com/spf13/cobra" ) @@ -110,7 +109,7 @@ func (c *DeployAppCommand) Exec(ctx context.Context) error { if err != nil { return fmt.Errorf("--allowed-tokens: %w", err) } - if info.Address == (ethCommon.Address{}) { + if info.Address == ETH_TOKEN { // ETH is always allowed by the guest — don't include it explicitly continue } diff --git a/wallet/go.mod b/wallet/go.mod index 40ce7e5..a2fa229 100644 --- a/wallet/go.mod +++ b/wallet/go.mod @@ -7,7 +7,7 @@ toolchain go1.24.3 // Can be useful for local developments replace github.com/HorizenOfficial/vela => ../../vela -//replace github.com/HorizenOfficial/vela-common-go => ../../vela-common-go +replace github.com/HorizenOfficial/vela-common-go => ../../vela-common-go replace github.com/HorizenOfficial/vela-nova/payment-app => ../runtime/wasm-go diff --git a/wallet/testutil/wallet_driver.go b/wallet/testutil/wallet_driver.go index 57028fb..ae11fec 100644 --- a/wallet/testutil/wallet_driver.go +++ b/wallet/testutil/wallet_driver.go @@ -27,6 +27,7 @@ import ( "strings" "testing" + velacommon "github.com/HorizenOfficial/vela-common-go/common" "github.com/HorizenOfficial/vela-nova/wallet/app" "github.com/HorizenOfficial/vela/pkg/blockchain" "github.com/HorizenOfficial/vela/pkg/common" @@ -240,7 +241,7 @@ func (d *WalletDriver) SetApplicationID(appID common.ApplicationIdType) { func (d *WalletDriver) AddToken(symbol string, address ethCommon.Address, decimals uint8) { d.t.Helper() require.NotEmpty(d.t, symbol, "AddToken: symbol must not be empty") - require.NotEqual(d.t, ethCommon.Address{}, address, "AddToken: zero address is reserved for ETH") + require.NotEqual(d.t, velacommon.ETH_TOKEN, address, "AddToken: zero address is reserved for ETH") upperSym := strings.ToUpper(symbol) entry := fmt.Sprintf("token.%s.address=%s\ntoken.%s.decimals=%d\n", upperSym, address.Hex(), upperSym, decimals) From 18dc76c3fd9ae3d7defd32653ec41751bad08ca1 Mon Sep 17 00:00:00 2001 From: Alberto Sala Date: Tue, 28 Apr 2026 18:03:25 +0200 Subject: [PATCH 22/25] Modifications after code review --- .../get_pending_payments_test.go | 106 ++++++++++++++++++ wallet/cmd/claimpendingpayments_test.go | 7 +- wallet/cmd/decryptreport_test.go | 4 +- wallet/cmd/deployapp_test.go | 4 +- wallet/cmd/deposit_test.go | 16 +-- wallet/cmd/getpendingpayments.go | 58 +++++----- wallet/cmd/getpendingpayments_test.go | 7 +- wallet/cmd/getpublicbalance_test.go | 10 +- wallet/cmd/helpers_test.go | 15 +++ wallet/cmd/privatetransfer_test.go | 4 +- wallet/cmd/registeruser_test.go | 10 +- wallet/cmd/requestreport_test.go | 4 +- wallet/cmd/withdraw_test.go | 7 +- wallet/testutil/wrappers.go | 21 ++++ 14 files changed, 193 insertions(+), 80 deletions(-) create mode 100644 runtime/wasm-go/fullstack_tests/get_pending_payments_test.go create mode 100644 wallet/cmd/helpers_test.go diff --git a/runtime/wasm-go/fullstack_tests/get_pending_payments_test.go b/runtime/wasm-go/fullstack_tests/get_pending_payments_test.go new file mode 100644 index 0000000..7c0a936 --- /dev/null +++ b/runtime/wasm-go/fullstack_tests/get_pending_payments_test.go @@ -0,0 +1,106 @@ +package main_test + +import ( + "os" + "path/filepath" + "testing" + + velacommon "github.com/HorizenOfficial/vela-common-go/common" + "github.com/HorizenOfficial/vela-nova/payment-app/testhelpers" + walletTestutil "github.com/HorizenOfficial/vela-nova/wallet/testutil" + "github.com/HorizenOfficial/vela/pkg/testutil/fullstack" + "github.com/stretchr/testify/require" +) + +// TestGetPendingPaymentsMatchesContract proves the wallet's GetPendingPayments +// command returns the same numeric value the ProcessorEndpoint contract holds +// in pendingClaims[ETH][user], and that the wallet's ClaimPendingPayments +// command zeros it out. Distinct from TestFeeRefundClaim, which exercises the +// accumulator and uses sim.Claim (the public claim path) bypassing the wallet: +// this test pins the read+claim flow specifically through the wallet code path. +// +// ## What's being exercised +// +// - GetPendingPaymentsCommand.Exec → BlockchainClient.GetPendingClaims: +// numeric agreement with the contract +// - ClaimPendingPaymentsCommand.Exec → BlockchainClient.Claim: clears the +// accumulator end-to-end through the wallet (TestFeeRefundClaim uses +// sim.Claim instead, so this is the only fullstack assertion that the +// wallet's claim wrapper actually clears state) +// +// ## Flow +// +// 1. Suite + executor + manager + driver. +// 2. Three happy-path requests (deploy + register + deposit) at maxFee=100 +// wei, accumulating non-trivial fee refunds in pendingClaims[ETH][user]. +// 3. walletAmount = driver.GetPendingPayments — read via the wallet path. +// contractAmount = sim.GetPendingClaims — read via the chain directly. +// Assert walletAmount == contractAmount > 0. +// 4. driver.ClaimPendingPayments — drive the wallet's claim path. +// 5. Re-read both: walletAmount == contractAmount == 0. +// 6. AssertNoStateUpdateErrors — pure happy-path; the wrapper's error +// channel should be empty. +func TestGetPendingPaymentsMatchesContract(t *testing.T) { + if os.Getenv("CI_FLAG") != "" { + t.Skip("Skipping fullstack test in CI environment") + } + + t.Setenv("MANAGER_ARTIFACTS_PATH", t.TempDir()) + + suite := fullstack.NewFullStackSystemTestSuite(t, "wasmtime-payment", + testhelpers.NewNetworkLogConfig(), testhelpers.NewNetworkLogConfig()) + defer suite.Cleanup() + + require.NoError(t, suite.StartExecutor()) + require.NoError(t, suite.StartManager()) + + driver := walletTestutil.NewWalletDriver(t, suite) + user := driver.UserAddress() + + wasmBytes := testhelpers.BuildAndLoadWasmModule(t) + wasmPath := filepath.Join(t.TempDir(), "payment_app.wasm") + require.NoError(t, os.WriteFile(wasmPath, wasmBytes, 0o644)) + + appID, err := driver.DeployApp(t.Context(), wasmPath, "100 wei", nil) + require.NoError(t, err) + require.NotZero(t, appID) + require.NoError(t, driver.RegisterUser(t.Context(), "100 wei")) + require.NoError(t, driver.Deposit(t.Context(), "1 ETH", "", "100 wei")) + + // Step 3: wallet's view should equal the contract's view, and be > 0. + sim := suite.GetSimTestHelper() + ethAddr := velacommon.ETH_TOKEN + + walletAmount, err := driver.GetPendingPayments(t.Context(), "") + require.NoError(t, err) + require.Equal(t, 1, walletAmount.Sign(), + "wallet should report a strictly positive pending claim after three successful requests; got %s", + walletAmount.String()) + + contractAmount := sim.GetPendingClaims(ethAddr, user) + require.Equal(t, 0, walletAmount.Cmp(contractAmount), + "wallet's view of pending claims (%s) must equal the contract's view (%s)", + walletAmount.String(), contractAmount.String()) + + // Step 4: drive the claim through the wallet path. Distinct from + // TestFeeRefundClaim, which uses sim.Claim and bypasses the wallet. + require.NoError(t, driver.ClaimPendingPayments(t.Context(), "")) + + // Step 5: both views should now read zero. If the wallet's claim wrapper + // silently no-op'd, walletAmount would still match contractAmount but + // both would be non-zero — caught by the > 0 check inverted. + walletAfter, err := driver.GetPendingPayments(t.Context(), "") + require.NoError(t, err) + require.Equal(t, 0, walletAfter.Sign(), + "wallet should report zero pending claims after ClaimPendingPayments; got %s", + walletAfter.String()) + + contractAfter := sim.GetPendingClaims(ethAddr, user) + require.Equal(t, 0, contractAfter.Sign(), + "contract should report zero pending claims after ClaimPendingPayments; got %s", + contractAfter.String()) + + // Step 6: pure happy-path; nothing should have reverted on the manager's + // stateUpdate path. + suite.AssertNoStateUpdateErrors(t) +} diff --git a/wallet/cmd/claimpendingpayments_test.go b/wallet/cmd/claimpendingpayments_test.go index dae7d2e..a287f91 100644 --- a/wallet/cmd/claimpendingpayments_test.go +++ b/wallet/cmd/claimpendingpayments_test.go @@ -9,7 +9,6 @@ import ( "github.com/HorizenOfficial/vela-nova/wallet/app" "github.com/HorizenOfficial/vela-nova/wallet/cmd/testutil" "github.com/HorizenOfficial/vela/pkg/blockchain" - pestestutil "github.com/HorizenOfficial/vela/pkg/blockchain/testutil" "github.com/HorizenOfficial/vela/pkg/crypto" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -25,8 +24,7 @@ func TestClaimPendingPaymentsCmd(t *testing.T) { key1, err := crypto.GeneratePrivateKeySecp256k1() require.NoError(t, err) - autoMining, useMockContracts := true, true - testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, nil) + testHelper := setupSimTestHelper(t, nil) defer testHelper.Close() var blockchainClient blockchain.Client = testutil.SetupNewBlockChainClient(testHelper) @@ -56,8 +54,7 @@ func TestClaimPendingPaymentsCmdNoKey(t *testing.T) { require.NoError(t, err) os.Stdout = w - autoMining, useMockContracts := true, true - testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, nil) + testHelper := setupSimTestHelper(t, nil) defer testHelper.Close() var blockchainClient blockchain.Client = testutil.SetupNewBlockChainClient(testHelper) diff --git a/wallet/cmd/decryptreport_test.go b/wallet/cmd/decryptreport_test.go index ba9a630..a0b4f70 100644 --- a/wallet/cmd/decryptreport_test.go +++ b/wallet/cmd/decryptreport_test.go @@ -11,7 +11,6 @@ import ( commontestutil "github.com/HorizenOfficial/vela/pkg/common/testutil" "github.com/HorizenOfficial/vela-nova/wallet/app" "github.com/HorizenOfficial/vela-nova/wallet/cmd/testutil" - pestestutil "github.com/HorizenOfficial/vela/pkg/blockchain/testutil" "github.com/HorizenOfficial/vela/pkg/crypto" "github.com/stretchr/testify/require" "github.com/stretchr/testify/assert" @@ -64,8 +63,7 @@ func TestDecryptReport(t *testing.T) { os.Stdout = w //create client - autoMining, useMockContracts := true, true - testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, teeKey.PublicKey().Bytes()) + testHelper := setupSimTestHelper(t, teeKey.PublicKey().Bytes()) defer testHelper.Close() client := testutil.SetupNewBlockChainClient(testHelper) diff --git a/wallet/cmd/deployapp_test.go b/wallet/cmd/deployapp_test.go index 5c586d6..5e386fd 100644 --- a/wallet/cmd/deployapp_test.go +++ b/wallet/cmd/deployapp_test.go @@ -17,7 +17,6 @@ import ( "github.com/HorizenOfficial/vela-nova/wallet/app" cmdtestutil "github.com/HorizenOfficial/vela-nova/wallet/cmd/testutil" "github.com/HorizenOfficial/vela/pkg/blockchain" - velatestutil "github.com/HorizenOfficial/vela/pkg/blockchain/testutil" "github.com/HorizenOfficial/vela/pkg/common" cryptotypes "github.com/HorizenOfficial/vela/pkg/common/crypto" ethCommon "github.com/ethereum/go-ethereum/common" @@ -338,8 +337,7 @@ func TestDeployAppCommand_UnauthorizedDeployerReverts(t *testing.T) { })) defer artifactServer.Close() - autoMining, useMockContracts := true, true - testHelper := velatestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, nil) + testHelper := setupSimTestHelper(t, nil) defer testHelper.Close() // Create a blockchain client using the Submitter account, which does NOT have diff --git a/wallet/cmd/deposit_test.go b/wallet/cmd/deposit_test.go index 95a1581..57b61ce 100644 --- a/wallet/cmd/deposit_test.go +++ b/wallet/cmd/deposit_test.go @@ -11,7 +11,6 @@ import ( "github.com/HorizenOfficial/vela-nova/wallet/app" "github.com/HorizenOfficial/vela-nova/wallet/cmd/testutil" "github.com/HorizenOfficial/vela/pkg/blockchain" - pestestutil "github.com/HorizenOfficial/vela/pkg/blockchain/testutil" "github.com/HorizenOfficial/vela/pkg/common" "github.com/HorizenOfficial/vela/pkg/crypto" "github.com/stretchr/testify/assert" @@ -30,8 +29,7 @@ func TestDepositCmdInvalidDepositAmount(t *testing.T) { key2, err := crypto.GeneratePrivateKeyP521() require.NoError(t, err) - autoMining, useMockContracts := true, true - testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, nil) + testHelper := setupSimTestHelper(t, nil) defer testHelper.Close() var blockchainClient blockchain.Client = testutil.SetupNewBlockChainClient(testHelper) @@ -78,8 +76,7 @@ func TestDepositCmdInvalidMaxValueFee(t *testing.T) { key2, err := crypto.GeneratePrivateKeyP521() require.NoError(t, err) - autoMining, useMockContracts := true, true - testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, nil) + testHelper := setupSimTestHelper(t, nil) defer testHelper.Close() var blockchainClient blockchain.Client = testutil.SetupNewBlockChainClient(testHelper) @@ -126,8 +123,7 @@ func TestDepositCmd(t *testing.T) { key2, err := crypto.GeneratePrivateKeyP521() require.NoError(t, err) - autoMining, useMockContracts := true, true - testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, nil) + testHelper := setupSimTestHelper(t, nil) defer testHelper.Close() appID := testutil.DeployTestApplication(t, testHelper) @@ -179,8 +175,7 @@ func TestDepositCmdFailure(t *testing.T) { key2, err := crypto.GeneratePrivateKeyP521() require.NoError(t, err) - autoMining, useMockContracts := true, true - testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, nil) + testHelper := setupSimTestHelper(t, nil) defer testHelper.Close() appID := testutil.DeployTestApplication(t, testHelper) @@ -231,8 +226,7 @@ func TestDepositCmdUsesDefaultMaxValueFee(t *testing.T) { key2, err := crypto.GeneratePrivateKeyP521() require.NoError(t, err) - autoMining, useMockContracts := true, true - testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, nil) + testHelper := setupSimTestHelper(t, nil) defer testHelper.Close() appID := testutil.DeployTestApplication(t, testHelper) diff --git a/wallet/cmd/getpendingpayments.go b/wallet/cmd/getpendingpayments.go index 382b2de..7b5e51f 100644 --- a/wallet/cmd/getpendingpayments.go +++ b/wallet/cmd/getpendingpayments.go @@ -3,10 +3,11 @@ package cmd import ( "context" "fmt" + "math/big" - ethCommon "github.com/ethereum/go-ethereum/common" "github.com/HorizenOfficial/vela-nova/wallet/app" "github.com/HorizenOfficial/vela/pkg/blockchain" + ethCommon "github.com/ethereum/go-ethereum/common" "github.com/spf13/cobra" ) @@ -21,43 +22,44 @@ func NewGetPendingPaymentsCommand(config *app.Config, blockchainClient blockchai } } +// Exec runs the getpendingpayments flow outside of the Cobra wrapper. Returns +// (amount, tokenInfo, err) so test drivers can assert on the numeric pending +// amount; tokenInfo is returned alongside so the Cobra wrapper doesn't need to +// re-resolve the token just to format the output. +func (c *GetPendingPaymentsCommand) Exec(ctx context.Context) (*big.Int, *app.TokenInfo, error) { + if c.Config.KeySecp == nil { + return nil, nil, fmt.Errorf("Secp256k1 key not found in the wallet") + } + + tokenInfo, err := c.Config.Tokens.ResolveToken(c.token) + if err != nil { + return nil, nil, err + } + + if err := c.InitChainClient(ctx); err != nil { + return nil, tokenInfo, fmt.Errorf("connecting to rpc node: %w", err) + } + defer c.CloseClient() + + address := ethCommon.HexToAddress(c.Config.KeySecp.PublicKey().Address()) + amount, err := c.BlockchainClient.GetPendingClaims(ctx, tokenInfo.Address, address) + if err != nil { + return nil, tokenInfo, fmt.Errorf("retrieving pending claims: %w", err) + } + return amount, tokenInfo, nil +} + func (c *GetPendingPaymentsCommand) Command() *cobra.Command { cmd := &cobra.Command{ Use: "getpendingpayments", Short: `display pending payments for your public address (amounts coming from fee refunds or withdrawals not yet claimed)`, Long: `display pending payments for this wallet address (amounts coming from fee refunds or withdrawals not yet claimed) on the ProcessorEndpoint contract`, Run: func(cmd *cobra.Command, args []string) { - if c.Config.KeySecp == nil { - fmt.Println("Error: Secp256k1 key not found in the wallet") - return - } - - // Resolve token - tokenInfo, err := c.Config.Tokens.ResolveToken(c.token) + amount, tokenInfo, err := c.Exec(resolveContext(cmd)) if err != nil { fmt.Printf("Error: %v\n", err) return } - - ctx := context.Background() - if cmd != nil && cmd.Context() != nil { - ctx = cmd.Context() - } - if c.BlockchainClient == nil { - if err := c.InitChainClient(ctx); err != nil { - fmt.Printf("Error connecting to rpc node: %v\n", err) - return - } - } - defer c.CloseClient() - - address := ethCommon.HexToAddress(c.Config.KeySecp.PublicKey().Address()) - amount, err := c.BlockchainClient.GetPendingClaims(ctx, tokenInfo.Address, address) - if err != nil { - fmt.Printf("Error retrieving pending claims: %v\n", err) - return - } - fmt.Printf("Pending claims: %s\n", c.Config.Tokens.FormatAmount(amount, tokenInfo)) }, } diff --git a/wallet/cmd/getpendingpayments_test.go b/wallet/cmd/getpendingpayments_test.go index 49eeecf..a86f53f 100644 --- a/wallet/cmd/getpendingpayments_test.go +++ b/wallet/cmd/getpendingpayments_test.go @@ -9,7 +9,6 @@ import ( "github.com/HorizenOfficial/vela-nova/wallet/app" "github.com/HorizenOfficial/vela-nova/wallet/cmd/testutil" "github.com/HorizenOfficial/vela/pkg/blockchain" - pestestutil "github.com/HorizenOfficial/vela/pkg/blockchain/testutil" "github.com/HorizenOfficial/vela/pkg/crypto" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -25,8 +24,7 @@ func TestGetPendingPaymentsCmd(t *testing.T) { key1, err := crypto.GeneratePrivateKeySecp256k1() require.NoError(t, err) - autoMining, useMockContracts := true, true - testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, nil) + testHelper := setupSimTestHelper(t, nil) defer testHelper.Close() var blockchainClient blockchain.Client = testutil.SetupNewBlockChainClient(testHelper) @@ -56,8 +54,7 @@ func TestGetPendingPaymentsCmdNoKey(t *testing.T) { require.NoError(t, err) os.Stdout = w - autoMining, useMockContracts := true, true - testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, nil) + testHelper := setupSimTestHelper(t, nil) defer testHelper.Close() var blockchainClient blockchain.Client = testutil.SetupNewBlockChainClient(testHelper) diff --git a/wallet/cmd/getpublicbalance_test.go b/wallet/cmd/getpublicbalance_test.go index 9e8c582..4582ec3 100644 --- a/wallet/cmd/getpublicbalance_test.go +++ b/wallet/cmd/getpublicbalance_test.go @@ -8,7 +8,6 @@ import ( "testing" "github.com/HorizenOfficial/vela-nova/wallet/app" - pestestutil "github.com/HorizenOfficial/vela/pkg/blockchain/testutil" "github.com/HorizenOfficial/vela/pkg/crypto" ethCommon "github.com/ethereum/go-ethereum/common" "github.com/magiconair/properties" @@ -27,8 +26,7 @@ func TestGetPublicBalanceCmd_ZeroBalance(t *testing.T) { os.Stdout = w defer func() { os.Stdout = old }() - autoMining, useMockContracts := true, true - testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, nil) + testHelper := setupSimTestHelper(t, nil) defer testHelper.Close() key, err := crypto.GeneratePrivateKeySecp256k1() @@ -56,8 +54,7 @@ func TestGetPublicBalanceCmd_FundedBalance(t *testing.T) { os.Stdout = w defer func() { os.Stdout = old }() - autoMining, useMockContracts := true, true - testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, nil) + testHelper := setupSimTestHelper(t, nil) defer testHelper.Close() key, err := crypto.GeneratePrivateKeySecp256k1() @@ -85,8 +82,7 @@ func TestGetPublicBalanceCmd_FundedBalance(t *testing.T) { // non-18 decimals keep the test honest — a hardcoded 10^18 divisor in the // format path would surface here as the wrong number of whole tokens. func TestGetPublicBalanceCmd_ERC20_Balance(t *testing.T) { - autoMining, useMockContracts := true, true - testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, nil) + testHelper := setupSimTestHelper(t, nil) defer testHelper.Close() const mockDecimals = 6 diff --git a/wallet/cmd/helpers_test.go b/wallet/cmd/helpers_test.go new file mode 100644 index 0000000..c718877 --- /dev/null +++ b/wallet/cmd/helpers_test.go @@ -0,0 +1,15 @@ +package cmd + +import ( + "testing" + + pestestutil "github.com/HorizenOfficial/vela/pkg/blockchain/testutil" +) + +// setupSimTestHelper builds a SimTestHelper with the defaults every wallet/cmd +// test wants: auto-mining on, mock TEE contracts, no pre-set tee signer. Pass +// teeKey.PublicKey().Bytes() when the test exercises a code path that needs a +// real secp521r1 pubkey on-chain; otherwise pass nil. +func setupSimTestHelper(t *testing.T, teePubSecp521r1 []byte) *pestestutil.SimTestHelper { + return pestestutil.NewSimTestHelper(t, true, true, nil, teePubSecp521r1) +} diff --git a/wallet/cmd/privatetransfer_test.go b/wallet/cmd/privatetransfer_test.go index dfa17b2..b195b35 100644 --- a/wallet/cmd/privatetransfer_test.go +++ b/wallet/cmd/privatetransfer_test.go @@ -9,7 +9,6 @@ import ( "github.com/HorizenOfficial/vela-nova/wallet/app" "github.com/HorizenOfficial/vela-nova/wallet/cmd/testutil" - pestestutil "github.com/HorizenOfficial/vela/pkg/blockchain/testutil" "github.com/HorizenOfficial/vela/pkg/crypto" "github.com/stretchr/testify/assert" ) @@ -28,8 +27,7 @@ func TestPrivateTransfer(t *testing.T) { var key2, _ = crypto.GeneratePrivateKeyP521() var teeKey, _ = crypto.GeneratePrivateKeyP521() - autoMining, useMockContracts := true, true - testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, teeKey.PublicKey().Bytes()) + testHelper := setupSimTestHelper(t, teeKey.PublicKey().Bytes()) defer testHelper.Close() appID := testutil.DeployTestApplication(t, testHelper) diff --git a/wallet/cmd/registeruser_test.go b/wallet/cmd/registeruser_test.go index 9245a18..d63e014 100644 --- a/wallet/cmd/registeruser_test.go +++ b/wallet/cmd/registeruser_test.go @@ -11,7 +11,6 @@ import ( "github.com/HorizenOfficial/vela-nova/wallet/app" "github.com/HorizenOfficial/vela-nova/wallet/cmd/testutil" "github.com/HorizenOfficial/vela/pkg/blockchain" - pestestutil "github.com/HorizenOfficial/vela/pkg/blockchain/testutil" "github.com/HorizenOfficial/vela/pkg/crypto" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -27,8 +26,7 @@ func TestRegisterUserCmd(t *testing.T) { var key2, _ = crypto.GeneratePrivateKeyP521() teeKey, _ := crypto.GeneratePrivateKeyP521() - autoMining, useMockContracts := true, true - testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, teeKey.PublicKey().Bytes()) + testHelper := setupSimTestHelper(t, teeKey.PublicKey().Bytes()) defer testHelper.Close() appID := testutil.DeployTestApplication(t, testHelper) @@ -79,8 +77,7 @@ func TestRegisterUserCmdFailure(t *testing.T) { teeKey, err := crypto.GeneratePrivateKeyP521() require.NoError(t, err) - autoMining, useMockContracts := true, true - testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, teeKey.PublicKey().Bytes()) + testHelper := setupSimTestHelper(t, teeKey.PublicKey().Bytes()) defer testHelper.Close() appID := testutil.DeployTestApplication(t, testHelper) @@ -131,8 +128,7 @@ func TestRegisterUserCmdTimeout(t *testing.T) { teeKey, err := crypto.GeneratePrivateKeyP521() require.NoError(t, err) - autoMining, useMockContracts := true, true - testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, teeKey.PublicKey().Bytes()) + testHelper := setupSimTestHelper(t, teeKey.PublicKey().Bytes()) defer testHelper.Close() appID := testutil.DeployTestApplication(t, testHelper) diff --git a/wallet/cmd/requestreport_test.go b/wallet/cmd/requestreport_test.go index 411eb03..c0b8213 100644 --- a/wallet/cmd/requestreport_test.go +++ b/wallet/cmd/requestreport_test.go @@ -10,7 +10,6 @@ import ( "github.com/HorizenOfficial/vela-nova/wallet/app" "github.com/HorizenOfficial/vela-nova/wallet/cmd/testutil" - pestestutil "github.com/HorizenOfficial/vela/pkg/blockchain/testutil" cryptotypes "github.com/HorizenOfficial/vela/pkg/common/crypto" "github.com/HorizenOfficial/vela/pkg/crypto" "github.com/stretchr/testify/assert" @@ -20,8 +19,7 @@ import ( func TestRequestReportCmd(t *testing.T) { teeKey, err := crypto.GeneratePrivateKeyP521() require.NoError(t, err) - autoMining, useMockContracts := true, true - testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, teeKey.PublicKey().Bytes()) + testHelper := setupSimTestHelper(t, teeKey.PublicKey().Bytes()) defer testHelper.Close() appID := testutil.DeployTestApplication(t, testHelper) diff --git a/wallet/cmd/withdraw_test.go b/wallet/cmd/withdraw_test.go index 9caf0cc..5d47116 100644 --- a/wallet/cmd/withdraw_test.go +++ b/wallet/cmd/withdraw_test.go @@ -12,7 +12,6 @@ import ( "github.com/HorizenOfficial/vela-nova/wallet/app" "github.com/HorizenOfficial/vela-nova/wallet/cmd/testutil" "github.com/HorizenOfficial/vela/pkg/blockchain" - pestestutil "github.com/HorizenOfficial/vela/pkg/blockchain/testutil" "github.com/HorizenOfficial/vela/pkg/crypto" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -32,8 +31,7 @@ func TestWithdrawCmd(t *testing.T) { teeKey, err := crypto.GeneratePrivateKeyP521() require.NoError(t, err) - autoMining, useMockContracts := true, true - testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, teeKey.PublicKey().Bytes()) + testHelper := setupSimTestHelper(t, teeKey.PublicKey().Bytes()) defer testHelper.Close() appID := testutil.DeployTestApplication(t, testHelper) @@ -88,8 +86,7 @@ func TestWithdrawCmdFailure(t *testing.T) { teeKey, err := crypto.GeneratePrivateKeyP521() require.NoError(t, err) - autoMining, useMockContracts := true, true - testHelper := pestestutil.NewSimTestHelper(t, autoMining, useMockContracts, nil, teeKey.PublicKey().Bytes()) + testHelper := setupSimTestHelper(t, teeKey.PublicKey().Bytes()) defer testHelper.Close() appID := testutil.DeployTestApplication(t, testHelper) diff --git a/wallet/testutil/wrappers.go b/wallet/testutil/wrappers.go index 5a5c6ae..c0c23b5 100644 --- a/wallet/testutil/wrappers.go +++ b/wallet/testutil/wrappers.go @@ -189,6 +189,27 @@ func (d *WalletDriver) DownloadReport(ctx context.Context, reportIDHex, destPath return nil } +// GetPendingPayments returns the wallet's pending-claims amount for `token`. +// Lets tests assert on the wallet's view of pendingClaims accumulation +// (fee refunds, withdrawals not yet claimed) end-to-end through the wallet +// code path, not just by reading the contract directly. +func (d *WalletDriver) GetPendingPayments(ctx context.Context, token string) (*big.Int, error) { + d.t.Helper() + + cfg := d.loadConfig() + c := cmd.NewGetPendingPaymentsCommand(cfg, d.newUserClient()) + cobraCmd := c.Command() + if token != "" { + require.NoError(d.t, cobraCmd.Flags().Set("token", token)) + } + + amount, _, err := c.Exec(ctx) + if err != nil { + return nil, fmt.Errorf("wallet driver: GetPendingPayments: %w", err) + } + return amount, nil +} + // ClaimPendingPayments pulls pending claims for `token` from the // ProcessorEndpoint contract into the user's public balance. func (d *WalletDriver) ClaimPendingPayments(ctx context.Context, token string) error { From 67f0eacbaaac4d1d9729e6566ca59726d198ed27 Mon Sep 17 00:00:00 2001 From: Alberto Sala Date: Mon, 4 May 2026 11:12:11 +0200 Subject: [PATCH 23/25] Modifications after code review. Referencing tag v0.1.1 --- .../deanonymize_unauthorized_authority_test.go | 4 +--- runtime/wasm-go/fullstack_tests/fee_refund_claim_test.go | 3 +-- .../wasm-go/fullstack_tests/invalid_state_root_test.go | 4 +--- .../fullstack_tests/non_manager_state_update_test.go | 4 +--- .../fullstack_tests/private_transfer_unregistered_test.go | 4 +--- runtime/wasm-go/go.mod | 8 ++++---- runtime/wasm-go/go.sum | 6 ++++-- wallet/go.mod | 8 ++++---- wallet/go.sum | 6 ++++-- 9 files changed, 21 insertions(+), 26 deletions(-) diff --git a/runtime/wasm-go/fullstack_tests/deanonymize_unauthorized_authority_test.go b/runtime/wasm-go/fullstack_tests/deanonymize_unauthorized_authority_test.go index 1385272..81d9833 100644 --- a/runtime/wasm-go/fullstack_tests/deanonymize_unauthorized_authority_test.go +++ b/runtime/wasm-go/fullstack_tests/deanonymize_unauthorized_authority_test.go @@ -3,7 +3,6 @@ package main_test import ( "os" "path/filepath" - "strings" "testing" "github.com/HorizenOfficial/vela-nova/payment-app/testhelpers" @@ -111,8 +110,7 @@ func TestDeanonymizeRequestFromUnauthorizedAuthorityReverts(t *testing.T) { // error string. _, err = driver.RequestReport(t.Context(), "balances", "100 wei") require.Error(t, err, "deanonymize request from unauthorized account must fail at submitRequest time") - require.True(t, - strings.Contains(err.Error(), "AuthorityNotAllowed"), + require.Contains(t, err.Error(), "AuthorityNotAllowed", "expected AuthorityNotAllowed revert (ProcessorEndpoint's DEANONYMIZATION branch check); got: %v", err, ) diff --git a/runtime/wasm-go/fullstack_tests/fee_refund_claim_test.go b/runtime/wasm-go/fullstack_tests/fee_refund_claim_test.go index 92c2d9a..548853c 100644 --- a/runtime/wasm-go/fullstack_tests/fee_refund_claim_test.go +++ b/runtime/wasm-go/fullstack_tests/fee_refund_claim_test.go @@ -1,7 +1,6 @@ package main_test import ( - "context" "math/big" "os" "path/filepath" @@ -125,7 +124,7 @@ func TestFeeRefundClaim(t *testing.T) { // Step 4: snapshot the user's on-chain balance before the claim. After // the claim we expect this to increase by exactly refundTotal (deployer // pays the gas in step 5). - ctx := context.Background() + ctx := t.Context() balanceBefore, err := sim.Client().BalanceAt(ctx, user, nil) require.NoError(t, err) diff --git a/runtime/wasm-go/fullstack_tests/invalid_state_root_test.go b/runtime/wasm-go/fullstack_tests/invalid_state_root_test.go index 7d3d3e6..53afca9 100644 --- a/runtime/wasm-go/fullstack_tests/invalid_state_root_test.go +++ b/runtime/wasm-go/fullstack_tests/invalid_state_root_test.go @@ -4,7 +4,6 @@ import ( "math/big" "os" "path/filepath" - "strings" "testing" velacommon "github.com/HorizenOfficial/vela-common-go/common" @@ -160,8 +159,7 @@ func TestInvalidPrevStateRootRejection(t *testing.T) { // Guard 4 fails because prev=zero ≠ stored non-zero root. err = suite.SubmitStateUpdateAsManager(bogus) require.Error(t, err, "stateUpdate with stale prevStateRoot must be rejected") - require.True(t, - strings.Contains(err.Error(), "InvalidStateRoot"), + require.Contains(t, err.Error(), "InvalidStateRoot", "expected InvalidStateRoot revert (ProcessorEndpoint guard 4); got: %v", err, ) diff --git a/runtime/wasm-go/fullstack_tests/non_manager_state_update_test.go b/runtime/wasm-go/fullstack_tests/non_manager_state_update_test.go index 6ace669..f14dc9b 100644 --- a/runtime/wasm-go/fullstack_tests/non_manager_state_update_test.go +++ b/runtime/wasm-go/fullstack_tests/non_manager_state_update_test.go @@ -2,7 +2,6 @@ package main_test import ( "os" - "strings" "testing" "github.com/HorizenOfficial/vela-nova/payment-app/testhelpers" @@ -102,8 +101,7 @@ func TestNonManagerStateUpdateReverts(t *testing.T) { // Expect the OZ AccessControl revert. err = suite.SubmitStateUpdateAs(rogueKey, update) require.Error(t, err, "stateUpdate from non-manager account must be rejected") - require.True(t, - strings.Contains(err.Error(), "AccessControlUnauthorizedAccount"), + require.Contains(t, err.Error(), "AccessControlUnauthorizedAccount", "expected OZ AccessControlUnauthorizedAccount revert selector "+ "(stateUpdate's onlyRole(UPDATE_STATUS_ROLE) gate); got: %v", err, diff --git a/runtime/wasm-go/fullstack_tests/private_transfer_unregistered_test.go b/runtime/wasm-go/fullstack_tests/private_transfer_unregistered_test.go index 0668d18..7bc957d 100644 --- a/runtime/wasm-go/fullstack_tests/private_transfer_unregistered_test.go +++ b/runtime/wasm-go/fullstack_tests/private_transfer_unregistered_test.go @@ -4,7 +4,6 @@ import ( "math/big" "os" "path/filepath" - "strings" "testing" velacommon "github.com/HorizenOfficial/vela-common-go/common" @@ -142,8 +141,7 @@ func TestPrivateTransferToUnregisteredRecipient(t *testing.T) { // care about — a different code would indicate a different rejection // path (e.g., insufficient balance, invalid payload), which would mean // this test isn't testing what it thinks it's testing. - require.True(t, - strings.Contains(err.Error(), "code 9"), + require.Contains(t, err.Error(), "code 9", "expected PubKeyNotRegistered rejection (error code 9 from apperrors.CodePubKeyNotRegistered); got: %v", err, ) diff --git a/runtime/wasm-go/go.mod b/runtime/wasm-go/go.mod index 39b4b12..c778f7a 100644 --- a/runtime/wasm-go/go.mod +++ b/runtime/wasm-go/go.mod @@ -7,15 +7,15 @@ go 1.24.0 toolchain go1.24.3 // Can be useful for local developments -replace github.com/HorizenOfficial/vela => ../../../vela +//replace github.com/HorizenOfficial/vela => ../../../vela -replace github.com/HorizenOfficial/vela-common-go => ../../../vela-common-go +//replace github.com/HorizenOfficial/vela-common-go => ../../../vela-common-go replace github.com/HorizenOfficial/vela-nova/wallet => ../../wallet require ( - github.com/HorizenOfficial/vela v0.1.0 - github.com/HorizenOfficial/vela-common-go v0.1.0 + github.com/HorizenOfficial/vela v0.1.1 + github.com/HorizenOfficial/vela-common-go v0.1.1 github.com/HorizenOfficial/vela-nova/wallet v0.0.0-00010101000000-000000000000 github.com/ethereum/go-ethereum v1.17.2 github.com/stretchr/testify v1.11.1 diff --git a/runtime/wasm-go/go.sum b/runtime/wasm-go/go.sum index adc34a3..cde8c66 100644 --- a/runtime/wasm-go/go.sum +++ b/runtime/wasm-go/go.sum @@ -1,7 +1,9 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= -github.com/HorizenOfficial/vela-common-go v0.1.0 h1:tLtPH2O/61LRXvpn1AsEkFFrukEWODvfmBiFz1ijWBw= -github.com/HorizenOfficial/vela-common-go v0.1.0/go.mod h1:VGEDebTDUnwrnMycUHQtm9WBi0DpRcYjsetIU4KbRmc= +github.com/HorizenOfficial/vela v0.1.1 h1:QGVWN3c/w2Re/hWRR2fLSPcoFKCvK10SGXt/16weSqo= +github.com/HorizenOfficial/vela v0.1.1/go.mod h1:d7jf2jiAB3xMETvfWwLImKKhfDwJScoceQ4YnLxVfRU= +github.com/HorizenOfficial/vela-common-go v0.1.1 h1:apAD7gePzF+CDD72I8oTL1RLJNICthOKGdrpPzVQg/Y= +github.com/HorizenOfficial/vela-common-go v0.1.1/go.mod h1:VGEDebTDUnwrnMycUHQtm9WBi0DpRcYjsetIU4KbRmc= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= diff --git a/wallet/go.mod b/wallet/go.mod index a2fa229..1526984 100644 --- a/wallet/go.mod +++ b/wallet/go.mod @@ -5,15 +5,15 @@ go 1.24.0 toolchain go1.24.3 // Can be useful for local developments -replace github.com/HorizenOfficial/vela => ../../vela +//replace github.com/HorizenOfficial/vela => ../../vela -replace github.com/HorizenOfficial/vela-common-go => ../../vela-common-go +//replace github.com/HorizenOfficial/vela-common-go => ../../vela-common-go replace github.com/HorizenOfficial/vela-nova/payment-app => ../runtime/wasm-go require ( - github.com/HorizenOfficial/vela v0.1.0 - github.com/HorizenOfficial/vela-common-go v0.1.0 + github.com/HorizenOfficial/vela v0.1.1 + github.com/HorizenOfficial/vela-common-go v0.1.1 github.com/HorizenOfficial/vela-nova/payment-app v0.1.0 github.com/ethereum/go-ethereum v1.17.2 github.com/magiconair/properties v1.8.10 diff --git a/wallet/go.sum b/wallet/go.sum index adc34a3..cde8c66 100644 --- a/wallet/go.sum +++ b/wallet/go.sum @@ -1,7 +1,9 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= -github.com/HorizenOfficial/vela-common-go v0.1.0 h1:tLtPH2O/61LRXvpn1AsEkFFrukEWODvfmBiFz1ijWBw= -github.com/HorizenOfficial/vela-common-go v0.1.0/go.mod h1:VGEDebTDUnwrnMycUHQtm9WBi0DpRcYjsetIU4KbRmc= +github.com/HorizenOfficial/vela v0.1.1 h1:QGVWN3c/w2Re/hWRR2fLSPcoFKCvK10SGXt/16weSqo= +github.com/HorizenOfficial/vela v0.1.1/go.mod h1:d7jf2jiAB3xMETvfWwLImKKhfDwJScoceQ4YnLxVfRU= +github.com/HorizenOfficial/vela-common-go v0.1.1 h1:apAD7gePzF+CDD72I8oTL1RLJNICthOKGdrpPzVQg/Y= +github.com/HorizenOfficial/vela-common-go v0.1.1/go.mod h1:VGEDebTDUnwrnMycUHQtm9WBi0DpRcYjsetIU4KbRmc= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= From 1c27092c6112ba27c91a413738f4a83c7a5dda24 Mon Sep 17 00:00:00 2001 From: saratnt Date: Mon, 29 Jun 2026 11:39:47 +0200 Subject: [PATCH 24/25] Fix in signature for downloading report --- wallet/app/authority_client.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wallet/app/authority_client.go b/wallet/app/authority_client.go index 196f0e4..6060165 100644 --- a/wallet/app/authority_client.go +++ b/wallet/app/authority_client.go @@ -88,7 +88,8 @@ func (c *AuthorityClient) FetchReport(ctx context.Context, reportIDHex string, n } msg := authorityapi.BuildMessage(c.ChainID, c.AppID, reportID, nonceBytes) - hash := ethCrypto.Keccak256Hash(msg) + prefix := fmt.Sprintf("\x19Ethereum Signed Message:\n%d", len(msg)) + hash := ethCrypto.Keccak256Hash([]byte(prefix), msg) sig, err := ethCrypto.Sign(hash.Bytes(), c.KeySecp.PrivateKey) if err != nil { From 95f4e1bbc57f69481294babf5b7b63801bea24ba Mon Sep 17 00:00:00 2001 From: saratnt Date: Thu, 2 Jul 2026 14:01:15 +0200 Subject: [PATCH 25/25] Changes for v0.2.0 --- CLAUDE.md | 4 ++++ NOTICES | 20 ++++++++++---------- runtime/wasm-go/go.mod | 4 ++-- runtime/wasm-go/go.sum | 4 ++++ wallet/go.mod | 4 ++-- wallet/go.sum | 4 ++++ 6 files changed, 26 insertions(+), 14 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 769fbd3..f01df72 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -88,3 +88,7 @@ Deploy note: the sender used by `novaw deployapp` must have `DEPLOYER_ROLE` on t ## CI / release `.github/workflows/ci.yml` runs the wasm-go suite twice (fast + full), then the wallet suite, using Go 1.24 and TinyGo 0.39.0. Tags matching `v*` trigger a release that ships `production_build/payment_app.wasm`, `wallet/releases/novaw-linux`, and `wallet.conf.template` as GitHub Release artifacts. Bumping the Vela dependency version typically requires tagging both `vela` and `vela-common-go` at matching versions and updating both `go.mod` files together. + +## Dependencies & NOTICES + +Whenever dependencies change (any edit to either `go.mod` — version bumps, added, or removed modules), update the `NOTICES` file in the same change so its listed versions, licenses, and source links stay in sync with the `go.mod` files. `NOTICES` tracks the go-ethereum LGPL-3.0 relinking obligation and all third-party notices, so drift here is a licensing-compliance issue, not just documentation. diff --git a/NOTICES b/NOTICES index 818058b..edd9b8f 100644 --- a/NOTICES +++ b/NOTICES @@ -22,7 +22,7 @@ Source code and build configuration (go.mod, go.sum) are provided to enable this. go-ethereum - Version: v1.16.1 + Version: v1.17.2 License: GNU Lesser General Public License v3.0 (LGPL-3.0) (library code) GNU General Public License v3.0 (GPL-3.0) (cmd/ binaries only) Source: https://github.com/ethereum/go-ethereum @@ -40,13 +40,13 @@ PERMISSIVE DEPENDENCIES - Go (Direct) ================================================================================ vela-common-go - Version: v0.0.25 + Version: v0.2.0 License: BSL-1.1 Source: https://github.com/HorizenOfficial/vela-common-go Notice: Copyright (c) 2025 The Horizen Foundation. vela - Version: v0.0.25 + Version: v0.2.0 License: BSL-1.1 Source: https://github.com/HorizenOfficial/vela Notice: Copyright (c) 2025 The Horizen Foundation. @@ -86,25 +86,25 @@ orderedmap Notice: Copyright (c) 2020 Elliot Chance. gnark-crypto - Version: v0.18.0 + Version: v0.18.1 License: Apache-2.0 Source: https://github.com/consensys/gnark-crypto Notice: Copyright (c) 2020 ConsenSys Software Inc. go-eth-kzg - Version: v1.3.0 + Version: v1.5.0 License: Apache-2.0 Source: https://github.com/crate-crypto/go-eth-kzg Notice: Copyright (c) 2023 Crate Crypto. c-kzg-4844 - Version: v2.1.0 + Version: v2.1.6 License: Apache-2.0 Source: https://github.com/ethereum/c-kzg-4844 Notice: Copyright (c) 2023 The Ethereum Foundation. blst - Version: v0.3.14 + Version: v0.3.16 License: Apache-2.0 Source: https://github.com/supranational/blst Notice: Copyright (c) 2022 Supranational LLC. @@ -128,19 +128,19 @@ zerolog Notice: Copyright (c) 2017 Olivier Poitrey. golang/snappy - Version: v0.0.5-0.20220116011046-fa5810519dcb + Version: v1.0.0 License: BSD-3-Clause Source: https://github.com/golang/snappy Notice: Copyright 2011 The Snappy-Go Authors. golang.org/x/crypto - Version: v0.39.0 + Version: v0.44.0 License: BSD-3-Clause Source: https://github.com/golang/crypto Notice: Copyright 2009 The Go Authors. google.golang.org/protobuf - Version: v1.34.2 + Version: v1.36.11 License: BSD-3-Clause Source: https://github.com/protocolbuffers/protobuf-go Notice: Copyright 2018 The Go Authors. diff --git a/runtime/wasm-go/go.mod b/runtime/wasm-go/go.mod index c778f7a..140859c 100644 --- a/runtime/wasm-go/go.mod +++ b/runtime/wasm-go/go.mod @@ -14,8 +14,8 @@ toolchain go1.24.3 replace github.com/HorizenOfficial/vela-nova/wallet => ../../wallet require ( - github.com/HorizenOfficial/vela v0.1.1 - github.com/HorizenOfficial/vela-common-go v0.1.1 + github.com/HorizenOfficial/vela v0.2.0 + github.com/HorizenOfficial/vela-common-go v0.2.0 github.com/HorizenOfficial/vela-nova/wallet v0.0.0-00010101000000-000000000000 github.com/ethereum/go-ethereum v1.17.2 github.com/stretchr/testify v1.11.1 diff --git a/runtime/wasm-go/go.sum b/runtime/wasm-go/go.sum index cde8c66..43dff41 100644 --- a/runtime/wasm-go/go.sum +++ b/runtime/wasm-go/go.sum @@ -2,8 +2,12 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/HorizenOfficial/vela v0.1.1 h1:QGVWN3c/w2Re/hWRR2fLSPcoFKCvK10SGXt/16weSqo= github.com/HorizenOfficial/vela v0.1.1/go.mod h1:d7jf2jiAB3xMETvfWwLImKKhfDwJScoceQ4YnLxVfRU= +github.com/HorizenOfficial/vela v0.2.0 h1:VyyOY+eDau07VFW8L6EOGyXAS1e0oxI6eflzaj0qZi8= +github.com/HorizenOfficial/vela v0.2.0/go.mod h1:+JecgRDkVNb8bFxJHfmRtp5TOKJ+TR/ecQ2Rms/6xaw= github.com/HorizenOfficial/vela-common-go v0.1.1 h1:apAD7gePzF+CDD72I8oTL1RLJNICthOKGdrpPzVQg/Y= github.com/HorizenOfficial/vela-common-go v0.1.1/go.mod h1:VGEDebTDUnwrnMycUHQtm9WBi0DpRcYjsetIU4KbRmc= +github.com/HorizenOfficial/vela-common-go v0.2.0 h1:aMyqvuRIpoFnod8j129EBMk09L074Urcef83HmBHuA0= +github.com/HorizenOfficial/vela-common-go v0.2.0/go.mod h1:VGEDebTDUnwrnMycUHQtm9WBi0DpRcYjsetIU4KbRmc= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= diff --git a/wallet/go.mod b/wallet/go.mod index 1526984..7726ce2 100644 --- a/wallet/go.mod +++ b/wallet/go.mod @@ -12,8 +12,8 @@ toolchain go1.24.3 replace github.com/HorizenOfficial/vela-nova/payment-app => ../runtime/wasm-go require ( - github.com/HorizenOfficial/vela v0.1.1 - github.com/HorizenOfficial/vela-common-go v0.1.1 + github.com/HorizenOfficial/vela v0.2.0 + github.com/HorizenOfficial/vela-common-go v0.2.0 github.com/HorizenOfficial/vela-nova/payment-app v0.1.0 github.com/ethereum/go-ethereum v1.17.2 github.com/magiconair/properties v1.8.10 diff --git a/wallet/go.sum b/wallet/go.sum index cde8c66..43dff41 100644 --- a/wallet/go.sum +++ b/wallet/go.sum @@ -2,8 +2,12 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/HorizenOfficial/vela v0.1.1 h1:QGVWN3c/w2Re/hWRR2fLSPcoFKCvK10SGXt/16weSqo= github.com/HorizenOfficial/vela v0.1.1/go.mod h1:d7jf2jiAB3xMETvfWwLImKKhfDwJScoceQ4YnLxVfRU= +github.com/HorizenOfficial/vela v0.2.0 h1:VyyOY+eDau07VFW8L6EOGyXAS1e0oxI6eflzaj0qZi8= +github.com/HorizenOfficial/vela v0.2.0/go.mod h1:+JecgRDkVNb8bFxJHfmRtp5TOKJ+TR/ecQ2Rms/6xaw= github.com/HorizenOfficial/vela-common-go v0.1.1 h1:apAD7gePzF+CDD72I8oTL1RLJNICthOKGdrpPzVQg/Y= github.com/HorizenOfficial/vela-common-go v0.1.1/go.mod h1:VGEDebTDUnwrnMycUHQtm9WBi0DpRcYjsetIU4KbRmc= +github.com/HorizenOfficial/vela-common-go v0.2.0 h1:aMyqvuRIpoFnod8j129EBMk09L074Urcef83HmBHuA0= +github.com/HorizenOfficial/vela-common-go v0.2.0/go.mod h1:VGEDebTDUnwrnMycUHQtm9WBi0DpRcYjsetIU4KbRmc= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU=