From 6637ec9a5d67d539fd865a17dc31f7145df3f1db Mon Sep 17 00:00:00 2001 From: Matee Ullah Malik Date: Mon, 1 Dec 2025 20:21:39 +0500 Subject: [PATCH 1/2] Fix ic, add action debug helpers. Match Index file format with sdk-js --- pkg/cascadekit/index.go | 10 +- sdk/action/client.go | 10 +- supernode/cascade/debug_action.go | 137 +++++++++++++++++++++++++ supernode/cascade/debug_action_test.go | 94 +++++++++++++++++ 4 files changed, 243 insertions(+), 8 deletions(-) create mode 100644 supernode/cascade/debug_action.go create mode 100644 supernode/cascade/debug_action_test.go diff --git a/pkg/cascadekit/index.go b/pkg/cascadekit/index.go index 456b365f..8e90f10b 100644 --- a/pkg/cascadekit/index.go +++ b/pkg/cascadekit/index.go @@ -14,14 +14,20 @@ const SeparatorByte byte = 46 // IndexFile represents the structure of the index file referenced on-chain. // The JSON fields must match the existing format. type IndexFile struct { - Version int `json:"version,omitempty"` + // Note: field order is chosen to match the JS SDK's canonical JSON: + // {"layout_ids":[...],"layout_signature":"...","version":1} LayoutIDs []string `json:"layout_ids"` LayoutSignature string `json:"layout_signature"` + Version int `json:"version,omitempty"` } // BuildIndex creates an IndexFile from layout IDs and the layout signature. func BuildIndex(layoutIDs []string, layoutSigB64 string) IndexFile { - return IndexFile{LayoutIDs: layoutIDs, LayoutSignature: layoutSigB64} + return IndexFile{ + LayoutIDs: layoutIDs, + LayoutSignature: layoutSigB64, + Version: 1, + } } // EncodeIndexB64 marshals an index file and returns its base64-encoded JSON. diff --git a/sdk/action/client.go b/sdk/action/client.go index 09eea748..46966064 100644 --- a/sdk/action/client.go +++ b/sdk/action/client.go @@ -2,8 +2,10 @@ package action import ( "context" + crand "crypto/rand" "encoding/base64" "fmt" + "math/big" "os" "path/filepath" "strconv" @@ -282,8 +284,8 @@ func (c *ClientImpl) BuildCascadeMetadataFromFile(ctx context.Context, filePath max = 50 } // Pick a random initial counter in [1,100] - //rnd, _ := crand.Int(crand.Reader, big.NewInt(100)) - ic := uint32(6) + rnd, _ := crand.Int(crand.Reader, big.NewInt(100)) + ic := uint32(rnd.Int64() + 1) // 1..100 // Create signatures from the layout struct using ADR-36 scheme (JS compatible). indexSignatureFormat, _, err := cascadekit.CreateSignaturesWithKeyringADR36( @@ -386,7 +388,3 @@ func (c *ClientImpl) GenerateDownloadSignature(ctx context.Context, actionID, cr } return base64.StdEncoding.EncodeToString(sig), nil } - -func (c *ClientImpl) signerAddress() string { - return c.signerAddr -} diff --git a/supernode/cascade/debug_action.go b/supernode/cascade/debug_action.go new file mode 100644 index 00000000..a97fd1e9 --- /dev/null +++ b/supernode/cascade/debug_action.go @@ -0,0 +1,137 @@ +package cascade + +import ( + "context" + "encoding/json" + "fmt" + + actiontypes "github.com/LumeraProtocol/lumera/x/action/v1/types" + "github.com/LumeraProtocol/supernode/v2/pkg/cascadekit" +) + +// DebugReverseEngineerAction fetches an action by ID, decodes its Cascade +// metadata and embedded index information, and prints a detailed breakdown. +// It performs read-only inspection and is intended for internal debugging. +func (task *CascadeRegistrationTask) DebugReverseEngineerAction(ctx context.Context, actionID string) error { + _ = ctx // kept for parity with other methods; not used here + + if actionID == "" { + fmt.Println("DebugReverseEngineerAction: empty actionID, nothing to do") + return nil + } + + fmt.Printf("DebugReverseEngineerAction: fetching action %s\n", actionID) + + // Step 1: Fetch the action from the chain + action, err := task.fetchAction(context.Background(), actionID, nil) + if err != nil { + fmt.Printf("DebugReverseEngineerAction: error fetching action %s: %v\n", actionID, err) + return err + } + + // Step 2: Print basic action summary + fmt.Println("=== Action Summary ===") + fmt.Printf("Action ID : %s\n", action.ActionID) + fmt.Printf("Creator : %s\n", action.Creator) + fmt.Printf("Action Type : %s\n", action.ActionType.String()) + fmt.Printf("State : %s\n", action.State.String()) + fmt.Printf("Block Height : %d\n", action.BlockHeight) + fmt.Printf("Price : %s\n", action.Price) + fmt.Printf("Expiration (unix): %d\n", action.ExpirationTime) + fmt.Printf("Metadata bytes : %d\n", len(action.Metadata)) + fmt.Printf("Supernodes (%d) : %v\n", len(action.SuperNodes), action.SuperNodes) + + // Only Cascade actions carry CascadeMetadata + if action.ActionType != actiontypes.ActionTypeCascade { + fmt.Println("Action is not of type CASCADE; skipping cascade-specific decoding.") + return nil + } + + if len(action.Metadata) == 0 { + fmt.Println("Cascade action has empty metadata; nothing to decode.") + return nil + } + + // Step 3: Decode Cascade metadata + cascadeMeta, err := cascadekit.UnmarshalCascadeMetadata(action.Metadata) + if err != nil { + fmt.Printf("Failed to unmarshal cascade metadata: %v\n", err) + return err + } + + fmt.Println("\n=== Cascade Metadata (summary) ===") + fmt.Printf("Data hash (b64) : %s\n", cascadeMeta.DataHash) + fmt.Printf("File name : %s\n", cascadeMeta.FileName) + fmt.Printf("rq_ids_ic : %d\n", cascadeMeta.RqIdsIc) + fmt.Printf("rq_ids_max : %d\n", cascadeMeta.RqIdsMax) + fmt.Printf("rq_ids_ids (#) : %d\n", len(cascadeMeta.RqIdsIds)) + fmt.Printf("Public : %v\n", cascadeMeta.Public) + if len(cascadeMeta.RqIdsIds) > 0 { + fmt.Printf("rq_ids_ids : %v\n", cascadeMeta.RqIdsIds) + } + fmt.Printf("Signatures len : %d\n", len(cascadeMeta.Signatures)) + + if metaJSON, mErr := json.MarshalIndent(cascadeMeta, "", " "); mErr == nil { + fmt.Println("\n=== Cascade Metadata (JSON) ===") + fmt.Println(string(metaJSON)) + } + + // Step 4: Decode index information from the signatures field + if cascadeMeta.Signatures == "" { + fmt.Println("\nCascade metadata has empty signatures field; cannot decode index.") + return nil + } + + indexB64, creatorSigB64, err := cascadekit.ExtractIndexAndCreatorSig(cascadeMeta.Signatures) + if err != nil { + fmt.Printf("Failed to extract index and creator signature: %v\n", err) + return err + } + + fmt.Println("\n=== Index Signature Components ===") + fmt.Printf("index_b64 length : %d\n", len(indexB64)) + fmt.Printf("creator_sig_b64 length : %d\n", len(creatorSigB64)) + if len(indexB64) > 0 { + previewLen := 64 + if len(indexB64) < previewLen { + previewLen = len(indexB64) + } + fmt.Printf("index_b64 prefix : %s\n", indexB64[:previewLen]) + } + if len(creatorSigB64) > 0 { + previewLen := 64 + if len(creatorSigB64) < previewLen { + previewLen = len(creatorSigB64) + } + fmt.Printf("creator_sig_b64 prefix : %s\n", creatorSigB64[:previewLen]) + } + + // Step 5: Decode the logical index file (base64(JSON(IndexFile))) + indexFile, err := cascadekit.DecodeIndexB64(indexB64) + if err != nil { + fmt.Printf("Failed to decode index file from base64: %v\n", err) + return err + } + + fmt.Println("\n=== Index File (summary) ===") + fmt.Printf("Version : %d\n", indexFile.Version) + fmt.Printf("Layout IDs (#) : %d\n", len(indexFile.LayoutIDs)) + fmt.Printf("Layout IDs : %v\n", indexFile.LayoutIDs) + fmt.Printf("Layout signature len : %d\n", len(indexFile.LayoutSignature)) + if layoutSig := indexFile.LayoutSignature; layoutSig != "" { + previewLen := 64 + if len(layoutSig) < previewLen { + previewLen = len(layoutSig) + } + fmt.Printf("Layout signature prefix: %s\n", layoutSig[:previewLen]) + } + + if indexJSON, iErr := json.MarshalIndent(indexFile, "", " "); iErr == nil { + fmt.Println("\n=== Index File (JSON) ===") + fmt.Println(string(indexJSON)) + } + + fmt.Println("\nDebugReverseEngineerAction: completed successfully.") + + return nil +} diff --git a/supernode/cascade/debug_action_test.go b/supernode/cascade/debug_action_test.go new file mode 100644 index 00000000..040cd9dd --- /dev/null +++ b/supernode/cascade/debug_action_test.go @@ -0,0 +1,94 @@ +package cascade + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/LumeraProtocol/supernode/v2/pkg/keyring" + "github.com/LumeraProtocol/supernode/v2/pkg/logtrace" + "github.com/LumeraProtocol/supernode/v2/pkg/lumera" + "github.com/LumeraProtocol/supernode/v2/supernode/config" +) + +// debugActionID is intentionally a constant so it is easy to change +// and re-run this helper test from VS Code for different actions. +// Set it to a real on-chain Cascade action ID before running. +const debugActionID = "10113" + +// TestDebugReverseEngineerAction is a convenience wrapper around +// DebugReverseEngineerAction. It is not a real unit test; it simply +// wires up configuration and logs a detailed breakdown for a single +// action ID, making it easy to inspect from VS Code. +func TestDebugReverseEngineerAction(t *testing.T) { + if debugActionID == "" { + t.Skip("set debugActionID to a real action ID to run this debug helper") + } + + // Initialize Cosmos SDK config (Bech32 prefixes, etc.). + keyring.InitSDKConfig() + + // Use the same logging setup as the supernode binary for consistent output. + logtrace.Setup("supernode-debug") + + ctx := context.Background() + + // Derive base directory and config path as in the supernode CLI. + homeDir, err := os.UserHomeDir() + if err != nil { + t.Fatalf("failed to get home directory: %v", err) + } + baseDir := filepath.Join(homeDir, ".supernode") + cfgFile := filepath.Join(baseDir, "config.yml") + + cfg, err := config.LoadConfig(cfgFile, baseDir) + if err != nil { + t.Fatalf("failed to load supernode config from %s: %v", cfgFile, err) + } + + // Initialize keyring using the configured directory. + keyringCfg := cfg.KeyringConfig + keyringCfg.Dir = cfg.GetKeyringDir() + + kr, err := keyring.InitKeyring(keyringCfg) + if err != nil { + t.Fatalf("failed to initialize keyring: %v", err) + } + + // Initialize Lumera client using the same configuration as the supernode. + lumeraCfg, err := lumera.NewConfig( + cfg.LumeraClientConfig.GRPCAddr, + cfg.LumeraClientConfig.ChainID, + cfg.SupernodeConfig.KeyName, + kr, + ) + if err != nil { + t.Fatalf("failed to create Lumera config: %v", err) + } + + lumeraClient, err := lumera.NewClient(ctx, lumeraCfg) + if err != nil { + t.Fatalf("failed to create Lumera client: %v", err) + } + defer func() { + _ = lumeraClient.Close() + }() + + // We only need the Lumera client for this debug helper; P2P and codec + // are left nil because DebugReverseEngineerAction is read-only and + // does not depend on them. + service := NewCascadeService( + cfg.SupernodeConfig.Identity, + lumeraClient, + nil, // p2p.Client + nil, // codec.Codec + nil, // rqstore.Store + ) + + task := NewCascadeRegistrationTask(service) + + if err := task.DebugReverseEngineerAction(ctx, debugActionID); err != nil { + t.Fatalf("DebugReverseEngineerAction returned error: %v", err) + } +} From 7363e3dee267ce7df2dd457c1434c8cfbbdb90b0 Mon Sep 17 00:00:00 2001 From: Matee Ullah Malik Date: Tue, 2 Dec 2025 01:11:17 +0500 Subject: [PATCH 2/2] Fix unit tests in gh pipeline --- supernode/cascade/debug_action_test.go | 186 +++++++++++++------------ 1 file changed, 94 insertions(+), 92 deletions(-) diff --git a/supernode/cascade/debug_action_test.go b/supernode/cascade/debug_action_test.go index 040cd9dd..4dc3047f 100644 --- a/supernode/cascade/debug_action_test.go +++ b/supernode/cascade/debug_action_test.go @@ -1,94 +1,96 @@ package cascade -import ( - "context" - "os" - "path/filepath" - "testing" - - "github.com/LumeraProtocol/supernode/v2/pkg/keyring" - "github.com/LumeraProtocol/supernode/v2/pkg/logtrace" - "github.com/LumeraProtocol/supernode/v2/pkg/lumera" - "github.com/LumeraProtocol/supernode/v2/supernode/config" -) - -// debugActionID is intentionally a constant so it is easy to change -// and re-run this helper test from VS Code for different actions. -// Set it to a real on-chain Cascade action ID before running. -const debugActionID = "10113" - -// TestDebugReverseEngineerAction is a convenience wrapper around -// DebugReverseEngineerAction. It is not a real unit test; it simply -// wires up configuration and logs a detailed breakdown for a single -// action ID, making it easy to inspect from VS Code. -func TestDebugReverseEngineerAction(t *testing.T) { - if debugActionID == "" { - t.Skip("set debugActionID to a real action ID to run this debug helper") - } - - // Initialize Cosmos SDK config (Bech32 prefixes, etc.). - keyring.InitSDKConfig() - - // Use the same logging setup as the supernode binary for consistent output. - logtrace.Setup("supernode-debug") - - ctx := context.Background() - - // Derive base directory and config path as in the supernode CLI. - homeDir, err := os.UserHomeDir() - if err != nil { - t.Fatalf("failed to get home directory: %v", err) - } - baseDir := filepath.Join(homeDir, ".supernode") - cfgFile := filepath.Join(baseDir, "config.yml") - - cfg, err := config.LoadConfig(cfgFile, baseDir) - if err != nil { - t.Fatalf("failed to load supernode config from %s: %v", cfgFile, err) - } - - // Initialize keyring using the configured directory. - keyringCfg := cfg.KeyringConfig - keyringCfg.Dir = cfg.GetKeyringDir() - - kr, err := keyring.InitKeyring(keyringCfg) - if err != nil { - t.Fatalf("failed to initialize keyring: %v", err) - } - - // Initialize Lumera client using the same configuration as the supernode. - lumeraCfg, err := lumera.NewConfig( - cfg.LumeraClientConfig.GRPCAddr, - cfg.LumeraClientConfig.ChainID, - cfg.SupernodeConfig.KeyName, - kr, - ) - if err != nil { - t.Fatalf("failed to create Lumera config: %v", err) - } - - lumeraClient, err := lumera.NewClient(ctx, lumeraCfg) - if err != nil { - t.Fatalf("failed to create Lumera client: %v", err) - } - defer func() { - _ = lumeraClient.Close() - }() - - // We only need the Lumera client for this debug helper; P2P and codec - // are left nil because DebugReverseEngineerAction is read-only and - // does not depend on them. - service := NewCascadeService( - cfg.SupernodeConfig.Identity, - lumeraClient, - nil, // p2p.Client - nil, // codec.Codec - nil, // rqstore.Store - ) - - task := NewCascadeRegistrationTask(service) - - if err := task.DebugReverseEngineerAction(ctx, debugActionID); err != nil { - t.Fatalf("DebugReverseEngineerAction returned error: %v", err) - } -} +//NOTE: Commented intentionally as this is not a real test rather a helper for manula debugging + +// import ( +// "context" +// "os" +// "path/filepath" +// "testing" + +// "github.com/LumeraProtocol/supernode/v2/pkg/keyring" +// "github.com/LumeraProtocol/supernode/v2/pkg/logtrace" +// "github.com/LumeraProtocol/supernode/v2/pkg/lumera" +// "github.com/LumeraProtocol/supernode/v2/supernode/config" +// ) + +// // debugActionID is intentionally a constant so it is easy to change +// // and re-run this helper test from VS Code for different actions. +// // Set it to a real on-chain Cascade action ID before running. +// const debugActionID = "10113" + +// // TestDebugReverseEngineerAction is a convenience wrapper around +// // DebugReverseEngineerAction. It is not a real unit test; it simply +// // wires up configuration and logs a detailed breakdown for a single +// // action ID, making it easy to inspect from VS Code. +// func TestDebugReverseEngineerAction(t *testing.T) { +// if debugActionID == "" { +// t.Skip("set debugActionID to a real action ID to run this debug helper") +// } + +// // Initialize Cosmos SDK config (Bech32 prefixes, etc.). +// keyring.InitSDKConfig() + +// // Use the same logging setup as the supernode binary for consistent output. +// logtrace.Setup("supernode-debug") + +// ctx := context.Background() + +// // Derive base directory and config path as in the supernode CLI. +// homeDir, err := os.UserHomeDir() +// if err != nil { +// t.Fatalf("failed to get home directory: %v", err) +// } +// baseDir := filepath.Join(homeDir, ".supernode") +// cfgFile := filepath.Join(baseDir, "config.yml") + +// cfg, err := config.LoadConfig(cfgFile, baseDir) +// if err != nil { +// t.Fatalf("failed to load supernode config from %s: %v", cfgFile, err) +// } + +// // Initialize keyring using the configured directory. +// keyringCfg := cfg.KeyringConfig +// keyringCfg.Dir = cfg.GetKeyringDir() + +// kr, err := keyring.InitKeyring(keyringCfg) +// if err != nil { +// t.Fatalf("failed to initialize keyring: %v", err) +// } + +// // Initialize Lumera client using the same configuration as the supernode. +// lumeraCfg, err := lumera.NewConfig( +// cfg.LumeraClientConfig.GRPCAddr, +// cfg.LumeraClientConfig.ChainID, +// cfg.SupernodeConfig.KeyName, +// kr, +// ) +// if err != nil { +// t.Fatalf("failed to create Lumera config: %v", err) +// } + +// lumeraClient, err := lumera.NewClient(ctx, lumeraCfg) +// if err != nil { +// t.Fatalf("failed to create Lumera client: %v", err) +// } +// defer func() { +// _ = lumeraClient.Close() +// }() + +// // We only need the Lumera client for this debug helper; P2P and codec +// // are left nil because DebugReverseEngineerAction is read-only and +// // does not depend on them. +// service := NewCascadeService( +// cfg.SupernodeConfig.Identity, +// lumeraClient, +// nil, // p2p.Client +// nil, // codec.Codec +// nil, // rqstore.Store +// ) + +// task := NewCascadeRegistrationTask(service) + +// if err := task.DebugReverseEngineerAction(ctx, debugActionID); err != nil { +// t.Fatalf("DebugReverseEngineerAction returned error: %v", err) +// } +// }