-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathe2e_cascade_test.go
More file actions
571 lines (483 loc) · 21 KB
/
e2e_cascade_test.go
File metadata and controls
571 lines (483 loc) · 21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
package system
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"github.com/LumeraProtocol/supernode/pkg/codec"
"github.com/LumeraProtocol/supernode/pkg/keyring"
"lukechampine.com/blake3"
"github.com/LumeraProtocol/supernode/sdk/action"
"github.com/LumeraProtocol/supernode/sdk/event"
"github.com/LumeraProtocol/lumera/x/action/types"
sdkconfig "github.com/LumeraProtocol/supernode/sdk/config"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
// TestCascadeE2E performs an end-to-end test of the Cascade functionality in the Lumera network.
// This test covers the entire process from initializing services, setting up accounts,
// creating and processing data through RaptorQ, submitting action requests to the blockchain,
// and monitoring the task execution to completion.
//
// The test demonstrates how data flows through the Lumera system:
// 1. Start services (blockchain, RaptorQ, supernode)
// 2. Set up test accounts and keys
// 3. Create test data and process it through RaptorQ
// 4. Sign the data and RQ identifiers
// 5. Submit a CASCADE action request with proper metadata
// 6. Execute the Cascade operation with the action ID
// 7. Monitor task completion and verify results
func TestCascadeE2E(t *testing.T) {
// ---------------------------------------
// Constants and Configuration Parameters
// ---------------------------------------
os.Setenv("SYSTEM_TEST", "true")
defer os.Unsetenv("SYSTEM_TEST")
// Test account credentials - these values are consistent across test runs
const testMnemonic = "odor kiss switch swarm spell make planet bundle skate ozone path planet exclude butter atom ahead angle royal shuffle door prevent merry alter robust"
const expectedAddress = "lumera1em87kgrvgttrkvuamtetyaagjrhnu3vjy44at4"
const testKeyName = "testkey1"
const userKeyName = "user"
const userMnemonic = "little tone alley oval festival gloom sting asthma crime select swap auto when trip luxury pact risk sister pencil about crisp upon opera timber"
const fundAmount = "1000000ulume"
// Network and service configuration constants
const (
raptorQHost = "localhost" // RaptorQ service host
raptorQPort = 50051 // RaptorQ service port
raptorQFilesDir = "./supernode-data/raptorq_files_test" // Directory for RaptorQ files
lumeraGRPCAddr = "localhost:9090" // Lumera blockchain GRPC address
lumeraChainID = "testing" // Lumera chain ID for testing
)
// Action request parameters
const (
actionType = "CASCADE" // The action type for fountain code processing
price = "23800ulume" // Price for the action in ulume tokens
)
t.Log("Step 1: Starting all services")
// Update the genesis file with action parameters
sut.ModifyGenesisJSON(t, SetActionParams(t))
// Reset and start the blockchain
sut.ResetChain(t)
sut.StartChain(t)
cli := NewLumeradCLI(t, sut, true)
// ---------------------------------------
// Register Multiple Supernodes to process the request
// ---------------------------------------
t.Log("Registering multiple supernodes to process requests")
// Helper function to register a supernode
registerSupernode := func(nodeKey string, port string, addr string) {
// Get account and validator addresses for registration
accountAddr := cli.GetKeyAddr(nodeKey)
valAddrOutput := cli.Keys("keys", "show", nodeKey, "--bech", "val", "-a")
valAddr := strings.TrimSpace(valAddrOutput)
t.Logf("Registering supernode for %s (validator: %s, account: %s)", nodeKey, valAddr, accountAddr)
// Register the supernode with the network
registerCmd := []string{
"tx", "supernode", "register-supernode",
valAddr, // validator address
"localhost:" + port, // IP address with unique port
"1.0.0", // version
addr, // supernode account
"--from", nodeKey,
}
resp := cli.CustomCommand(registerCmd...)
RequireTxSuccess(t, resp)
// Wait for transaction to be included in a block
sut.AwaitNextBlock(t)
}
// Register three supernodes with different ports
registerSupernode("node0", "4444", "lumera1em87kgrvgttrkvuamtetyaagjrhnu3vjy44at4")
registerSupernode("node1", "4446", "lumera1cf0ms9ttgdvz6zwlqfty4tjcawhuaq69p40w0c")
registerSupernode("node2", "4448", "lumera1cjyc4ruq739e2lakuhargejjkr0q5vg6x3d7kp")
t.Log("Successfully registered three supernodes")
// Fund Lume
cli.FundAddress("lumera1em87kgrvgttrkvuamtetyaagjrhnu3vjy44at4", "100000ulume")
cli.FundAddressWithNode("lumera1cf0ms9ttgdvz6zwlqfty4tjcawhuaq69p40w0c", "100000ulume", "node1")
cli.FundAddressWithNode("lumera1cjyc4ruq739e2lakuhargejjkr0q5vg6x3d7kp", "100000ulume", "node2")
queryHeight := sut.AwaitNextBlock(t)
args := []string{
"query",
"supernode",
"get-top-super-nodes-for-block",
fmt.Sprint(queryHeight),
"--output", "json",
}
// Get initial response to compare against
resp := cli.CustomQuery(args...)
t.Logf("Initial response: %s", resp)
// ---------------------------------------
// Step 1: Start all required services
// ---------------------------------------
// Start the supernode service to process cascade requests
cmds := StartAllSupernodes(t)
defer StopAllSupernodes(cmds)
// Ensure service is stopped after test
// ---------------------------------------
// Step 2: Set up test account and keys
// ---------------------------------------
t.Log("Step 2: Setting up test account")
// Locate and set up path to binary and home directory
binaryPath := locateExecutable(sut.ExecBinary)
homePath := filepath.Join(WorkDir, sut.outputDir)
// Add account key to the blockchain using the mnemonic
cmd := exec.Command(
binaryPath,
"keys", "add", testKeyName,
"--recover",
"--keyring-backend=test",
"--home", homePath,
)
cmd.Stdin = strings.NewReader(testMnemonic + "\n")
output, err := cmd.CombinedOutput()
require.NoError(t, err, "Key recovery failed: %s", string(output))
t.Logf("Key recovery output: %s", string(output))
// Create CLI helper and verify the address matches expected
recoveredAddress := cli.GetKeyAddr(testKeyName)
t.Logf("Recovered key %s with address: %s", testKeyName, recoveredAddress)
require.Equal(t, expectedAddress, recoveredAddress, "Recovered address should match expected address")
// Add user key to the blockchain using the provided mnemonic
cmd = exec.Command(
binaryPath,
"keys", "add", userKeyName,
"--recover",
"--keyring-backend=test",
"--home", homePath,
)
cmd.Stdin = strings.NewReader(userMnemonic + "\n")
output, err = cmd.CombinedOutput()
require.NoError(t, err, "User key recovery failed: %s", string(output))
t.Logf("User key recovery output: %s", string(output))
// Get the user address
userAddress := cli.GetKeyAddr(userKeyName)
t.Logf("Recovered user key with address: %s", userAddress)
// Fund the account with tokens for transactions
t.Logf("Funding test address %s with %s", recoveredAddress, fundAmount)
cli.FundAddress(recoveredAddress, fundAmount) // ulume tokens for action fees
cli.FundAddress(recoveredAddress, "10000000stake") // stake tokens
// Fund user account
t.Logf("Funding user address %s with %s", userAddress, fundAmount)
cli.FundAddress(userAddress, fundAmount) // ulume tokens for action fees
cli.FundAddress(userAddress, "10000000stake") // stake tokens
sut.AwaitNextBlock(t) // Wait for funding transaction to be processed
// Create an in-memory keyring for cryptographic operations
// This keyring is separate from the blockchain keyring and used for local signing
keplrKeyring, err := keyring.InitKeyring("memory", "")
require.NoError(t, err, "Failed to initialize in-memory keyring")
// Add the test key to the in-memory keyring
record, err := keyring.RecoverAccountFromMnemonic(keplrKeyring, testKeyName, testMnemonic)
require.NoError(t, err, "Failed to recover test account from mnemonic in local keyring")
// Also add the user key to the in-memory keyring
userRecord, err := keyring.RecoverAccountFromMnemonic(keplrKeyring, userKeyName, userMnemonic)
require.NoError(t, err, "Failed to recover user account from mnemonic in local keyring")
// Verify the addresses match between chain and local keyring
localAddr, err := record.GetAddress()
require.NoError(t, err, "Failed to get address from record")
require.Equal(t, expectedAddress, localAddr.String(),
"Local keyring address should match expected address")
t.Logf("Successfully recovered test key in local keyring with matching address: %s", localAddr.String())
userLocalAddr, err := userRecord.GetAddress()
require.NoError(t, err, "Failed to get user address from record")
require.Equal(t, userAddress, userLocalAddr.String(),
"User local keyring address should match user address")
t.Logf("Successfully recovered user key in local keyring with matching address: %s", userLocalAddr.String())
// Initialize Lumera blockchain client for interactions
//
require.NoError(t, err, "Failed to initialize Lumera client")
// ---------------------------------------
// Step 4: Create and prepare layout file for RaptorQ encoding
// ---------------------------------------
t.Log("Step 4: Creating test file for RaptorQ encoding")
// Create a test file with sample data in a temporary directory
testFileName := "testfile.txt"
testFileFullpath := filepath.Join(t.TempDir(), testFileName)
testData := []byte("This is test data for RaptorQ encoding in the Lumera nasaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaasassasetwork")
err = os.WriteFile(testFileFullpath, testData, 0644)
require.NoError(t, err, "Failed to write test file")
// Read the file into memory for processing
file, err := os.Open(testFileFullpath)
require.NoError(t, err, "Failed to open test file")
defer file.Close()
// Read the entire file content into a byte slice
fileInfo, err := file.Stat()
require.NoError(t, err, "Failed to get file stats")
data := make([]byte, fileInfo.Size())
_, err = io.ReadFull(file, data)
require.NoError(t, err, "Failed to read file contents")
t.Logf("Read %d bytes from test file", len(data))
rqCodec := codec.NewRaptorQCodec(raptorQFilesDir)
ctx := context.Background()
encodeRes, err := rqCodec.Encode(ctx, codec.EncodeRequest{
Data: data,
TaskID: "1",
})
require.NoError(t, err, "Failed to encode data with RaptorQ")
metadataFile := encodeRes.Metadata
// Marshal metadata to JSON and convert to bytes
me, err := json.Marshal(metadataFile)
require.NoError(t, err, "Failed to marshal metadata to JSON")
// Step 1: Encode the metadata JSON as base64 string
// This becomes the first part of our signature format
regularbase64EncodedData := base64.StdEncoding.EncodeToString(me)
t.Logf("Base64 encoded RQ IDs file length: %d", len(regularbase64EncodedData))
// Step 2: Sign the base64-encoded string with user key instead of testkey1
signedMetaData, err := keyring.SignBytes(keplrKeyring, userKeyName, []byte(regularbase64EncodedData))
require.NoError(t, err, "Failed to sign metadata")
// Step 3: Encode the resulting signature as base64
signedbase64EncodedData := base64.StdEncoding.EncodeToString(signedMetaData)
t.Logf("Base64 signed RQ IDs file length: %d", len(signedbase64EncodedData))
// Step 4: Format according to the expected verification pattern: Base64(rq_ids).signature
// This format is expected by VerifySignature in the CascadeActionHandler.RegisterAction method
// - regularbase64EncodedData: The base64-encoded metadata
// - signedbase64EncodedData: The base64-encoded signature of the above
signatureFormat := fmt.Sprintf("%s.%s", regularbase64EncodedData, signedbase64EncodedData)
t.Logf("Signature format prepared with length: %d bytes", len(signatureFormat))
// Data hash with blake3
hash, err := Blake3Hash(data)
b64EncodedHash := base64.StdEncoding.EncodeToString(hash)
require.NoError(t, err, "Failed to compute Blake3 hash")
// ---------------------------------------
t.Log("Step 7: Creating metadata and submitting action request")
// Create CascadeMetadata struct with all required fields
cascadeMetadata := types.CascadeMetadata{
DataHash: b64EncodedHash, // Hash of the original file
FileName: filepath.Base(testFileFullpath), // Original filename
RqIdsIc: uint64(121), // Count of RQ identifiers
Signatures: signatureFormat, // Combined signature format
}
// Marshal the struct to JSON for the blockchain transaction
metadataBytes, err := json.Marshal(cascadeMetadata)
require.NoError(t, err, "Failed to marshal CascadeMetadata to JSON")
metadata := string(metadataBytes)
// Set expiration time 25 hours in the future (minimum is 24 hours)
// This defines how long the action request is valid
expirationTime := fmt.Sprintf("%d", time.Now().Add(25*time.Hour).Unix())
t.Logf("Requesting cascade action with metadata: %s", metadata)
t.Logf("Action type: %s, Price: %s, Expiration: %s", actionType, price, expirationTime)
// Submit the action request transaction to the blockchain using user key
// This registers the request with metadata for supernodes to process
actionRequestResp := cli.CustomCommand(
"tx", "action", "request-action",
actionType, // CASCADE action type
metadata, // JSON metadata with all required fields
price, // Price in ulume tokens
expirationTime, // Unix timestamp for expiration
"--from", userKeyName, // Use user key for transaction submission
"--gas", "auto",
"--gas-adjustment", "1.5",
)
// Verify the transaction was successful
RequireTxSuccess(t, actionRequestResp)
t.Logf("Action request successful: %s", actionRequestResp)
// Wait for transaction to be included in a block
sut.AwaitNextBlock(t)
// Verify the account can be queried with its public key
accountResp := cli.CustomQuery("q", "auth", "account", userAddress)
require.Contains(t, accountResp, "public_key", "User account public key should be available")
// Extract transaction hash from response for verification
txHash := gjson.Get(actionRequestResp, "txhash").String()
require.NotEmpty(t, txHash, "Transaction hash should not be empty")
t.Logf("Transaction hash: %s", txHash)
// Query the transaction by hash to verify success and extract events
txResp := cli.CustomQuery("q", "tx", txHash)
t.Logf("Transaction query response: %s", txResp)
// Verify transaction code indicates success (0 = success)
txCode := gjson.Get(txResp, "code").Int()
require.Equal(t, int64(0), txCode, "Transaction should have success code 0")
// ---------------------------------------
// Step 8: Extract action ID and start cascade
// ---------------------------------------
t.Log("Step 8: Extracting action ID and creating cascade request")
// Extract action ID from transaction events
// The action_id is needed to reference this specific action in operations
events := gjson.Get(txResp, "events").Array()
var actionID string
for _, event := range events {
if event.Get("type").String() == "action_registered" {
attrs := event.Get("attributes").Array()
for _, attr := range attrs {
if attr.Get("key").String() == "action_id" {
actionID = attr.Get("value").String()
break
}
}
if actionID != "" {
break
}
}
}
require.NotEmpty(t, actionID, "Action ID should not be empty")
t.Logf("Extracted action ID: %s", actionID)
time.Sleep(60 * time.Second)
// Set up action client configuration
// This defines how to connect to network services
accConfig := sdkconfig.AccountConfig{
LocalCosmosAddress: recoveredAddress,
}
lumraConfig := sdkconfig.LumeraConfig{
GRPCAddr: lumeraGRPCAddr,
ChainID: lumeraChainID,
Timeout: 300, // 30 seconds timeout
KeyName: testKeyName,
}
actionConfig := sdkconfig.Config{
Account: accConfig,
Lumera: lumraConfig,
}
// Initialize action client for cascade operations
actionClient, err := action.NewClient(
ctx,
actionConfig,
nil, // Nil logger - use default
keplrKeyring, // Use the in-memory keyring for signing
)
require.NoError(t, err, "Failed to create action client")
// ---------------------------------------
// Step 9: Subscribe to all events and extract tx hash
// ---------------------------------------
// Channel to receive the transaction hash
txHashCh := make(chan string, 1)
completionCh := make(chan bool, 1)
// Subscribe to ALL events
err = actionClient.SubscribeToAllEvents(ctx, func(ctx context.Context, e event.Event) {
// Only capture TxhasReceived events
if e.Type == event.TxhasReceived {
if txHash, ok := e.Data["txhash"].(string); ok && txHash != "" {
// Send the hash to our channel
txHashCh <- txHash
}
}
// Also monitor for task completion
if e.Type == event.TaskCompleted {
completionCh <- true
}
})
require.NoError(t, err, "Failed to subscribe to events")
// Start cascade operation
t.Logf("Starting cascade operation with action ID: %s", actionID)
taskID, err := actionClient.StartCascade(
ctx,
data, // data []byte
actionID, // Action ID from the transaction
)
require.NoError(t, err, "Failed to start cascade operation")
t.Logf("Cascade operation started with task ID: %s", taskID)
recievedhash := <-txHashCh
<-completionCh
t.Logf("Received transaction hash: %s", recievedhash)
time.Sleep(10 * time.Second)
txReponse := cli.CustomQuery("q", "tx", recievedhash)
t.Logf("Transaction response: %s", txReponse)
// ---------------------------------------
// Step 10: Validate Transaction Events
// ---------------------------------------
t.Log("Step 9: Validating transaction events and payments")
// Check for action_finalized event
events = gjson.Get(txReponse, "events").Array()
var actionFinalized bool
var feeSpent bool
var feeReceived bool
var fromAddress string
var toAddress string
var amount string
for _, event := range events {
// Check for action finalized event
if event.Get("type").String() == "action_finalized" {
actionFinalized = true
attrs := event.Get("attributes").Array()
for _, attr := range attrs {
if attr.Get("key").String() == "action_type" {
require.Equal(t, "ACTION_TYPE_CASCADE", attr.Get("value").String(), "Action type should be CASCADE")
}
if attr.Get("key").String() == "action_id" {
require.Equal(t, actionID, attr.Get("value").String(), "Action ID should match")
}
}
}
// Check for fee spent event
if event.Get("type").String() == "coin_spent" {
attrs := event.Get("attributes").Array()
for i, attr := range attrs {
if attr.Get("key").String() == "amount" && attr.Get("value").String() == price {
feeSpent = true
// Get the spender address from the same event group
for j, addrAttr := range attrs {
if j < i && addrAttr.Get("key").String() == "spender" {
fromAddress = addrAttr.Get("value").String()
break
}
}
}
}
}
// Check for fee received event
if event.Get("type").String() == "coin_received" {
attrs := event.Get("attributes").Array()
for i, attr := range attrs {
if attr.Get("key").String() == "amount" && attr.Get("value").String() == price {
feeReceived = true
// Get the receiver address from the same event group
for j, addrAttr := range attrs {
if j < i && addrAttr.Get("key").String() == "receiver" {
toAddress = addrAttr.Get("value").String()
break
}
}
amount = attr.Get("value").String()
}
}
}
}
// Validate events
require.True(t, actionFinalized, "Action finalized event should be emitted")
require.True(t, feeSpent, "Fee spent event should be emitted")
require.True(t, feeReceived, "Fee received event should be emitted")
// Validate payment flow
t.Logf("Payment flow: %s paid %s to %s", fromAddress, amount, toAddress)
require.NotEmpty(t, fromAddress, "Spender address should not be empty")
require.NotEmpty(t, toAddress, "Receiver address should not be empty")
require.Equal(t, price, amount, "Payment amount should match action price")
t.Log("Test completed successfully!")
}
func Blake3Hash(msg []byte) ([]byte, error) {
hasher := blake3.New(32, nil)
if _, err := io.Copy(hasher, bytes.NewReader(msg)); err != nil {
return nil, err
}
return hasher.Sum(nil), nil
}
// SetActionParams sets the initial parameters for the action module in genesis
func SetActionParams(t *testing.T) GenesisMutator {
return func(genesis []byte) []byte {
t.Helper()
state, err := sjson.SetRawBytes(genesis, "app_state.action.params", []byte(`{
"base_action_fee": {
"amount": "10000",
"denom": "ulume"
},
"expiration_duration": "24h0m0s",
"fee_per_byte": {
"amount": "100",
"denom": "ulume"
},
"foundation_fee_share": "0.000000000000000000",
"max_actions_per_block": "10",
"max_dd_and_fingerprints": "50",
"max_processing_time": "1h0m0s",
"max_raptor_q_symbols": "50",
"min_processing_time": "1m0s",
"min_super_nodes": "1",
"super_node_fee_share": "1.000000000000000000"
}`))
require.NoError(t, err)
return state
}
}