Skip to content

Commit 066174d

Browse files
committed
tests(system): add happy-path self-healing e2e scenario
1 parent 39dc474 commit 066174d

1 file changed

Lines changed: 372 additions & 0 deletions

File tree

Lines changed: 372 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,372 @@
1+
package system
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
"strconv"
10+
"strings"
11+
"testing"
12+
"time"
13+
14+
actiontypes "github.com/LumeraProtocol/lumera/x/action/v1/types"
15+
"github.com/LumeraProtocol/lumera/x/lumeraid/securekeyx"
16+
pb "github.com/LumeraProtocol/supernode/v2/gen/supernode"
17+
"github.com/LumeraProtocol/supernode/v2/pkg/cascadekit"
18+
snkeyring "github.com/LumeraProtocol/supernode/v2/pkg/keyring"
19+
"github.com/LumeraProtocol/supernode/v2/pkg/lumera"
20+
"github.com/LumeraProtocol/supernode/v2/pkg/net/credentials"
21+
grpcclient "github.com/LumeraProtocol/supernode/v2/pkg/net/grpc/client"
22+
"github.com/LumeraProtocol/supernode/v2/sdk/action"
23+
sdkconfig "github.com/LumeraProtocol/supernode/v2/sdk/config"
24+
snconfig "github.com/LumeraProtocol/supernode/v2/supernode/config"
25+
"github.com/cosmos/cosmos-sdk/crypto/keyring"
26+
"github.com/stretchr/testify/require"
27+
"github.com/tidwall/gjson"
28+
)
29+
30+
const (
31+
shNode0KeyName = "testkey1"
32+
shNode1KeyName = "testkey2"
33+
shNode2KeyName = "testkey3"
34+
35+
shNode0Identity = "lumera1em87kgrvgttrkvuamtetyaagjrhnu3vjy44at4"
36+
shNode1Identity = "lumera1cf0ms9ttgdvz6zwlqfty4tjcawhuaq69p40w0c"
37+
shNode2Identity = "lumera1cjyc4ruq739e2lakuhargejjkr0q5vg6x3d7kp"
38+
39+
shUserKeyName = "user-sh"
40+
shUserMnemonic = "little tone alley oval festival gloom sting asthma crime select swap auto when trip luxury pact risk sister pencil about crisp upon opera timber"
41+
)
42+
43+
type selfHealingNode struct {
44+
KeyName string
45+
Identity string
46+
GRPCAddr string
47+
HasLocal bool
48+
}
49+
50+
type selfHealingRPCClient struct {
51+
client *grpcclient.Client
52+
opts *grpcclient.ClientOptions
53+
}
54+
55+
func TestSelfHealingE2EHappyPath(t *testing.T) {
56+
os.Setenv("INTEGRATION_TEST", "true")
57+
defer os.Unsetenv("INTEGRATION_TEST")
58+
59+
sut.ModifyGenesisJSON(t, SetStakingBondDenomUlume(t), SetActionParams(t), SetSupernodeMetricsParams(t))
60+
sut.StartChain(t)
61+
62+
cli := NewLumeradCLI(t, sut, true)
63+
registerSelfHealingSupernodes(t, cli)
64+
65+
cli.FundAddress(shNode0Identity, "100000ulume")
66+
cli.FundAddressWithNode(shNode1Identity, "100000ulume", "node1")
67+
cli.FundAddressWithNode(shNode2Identity, "100000ulume", "node2")
68+
69+
cmds := StartAllSupernodes(t)
70+
defer StopAllSupernodes(cmds)
71+
time.Sleep(8 * time.Second)
72+
73+
userAddress := cli.AddKeyFromSeed(shUserKeyName, shUserMnemonic)
74+
cli.FundAddress(userAddress, "1000000ulume")
75+
sut.AwaitNextBlock(t)
76+
77+
memKR, err := snkeyring.InitKeyring(snconfig.KeyringConfig{
78+
Backend: "memory",
79+
})
80+
require.NoError(t, err)
81+
_, err = snkeyring.RecoverAccountFromMnemonic(memKR, shUserKeyName, shUserMnemonic)
82+
require.NoError(t, err)
83+
84+
const lumeraGRPCAddr = "localhost:9090"
85+
const lumeraChainID = "testing"
86+
87+
userLumeraCfg, err := lumera.NewConfig(lumeraGRPCAddr, lumeraChainID, shUserKeyName, memKR)
88+
require.NoError(t, err)
89+
userLumera, err := lumera.NewClient(context.Background(), userLumeraCfg)
90+
require.NoError(t, err)
91+
defer func() { _ = userLumera.Close() }()
92+
93+
actionClient, err := action.NewClient(
94+
context.Background(),
95+
sdkconfig.Config{
96+
Account: sdkconfig.AccountConfig{
97+
KeyName: shUserKeyName,
98+
Keyring: memKR,
99+
},
100+
Lumera: sdkconfig.LumeraConfig{
101+
GRPCAddr: lumeraGRPCAddr,
102+
ChainID: lumeraChainID,
103+
},
104+
},
105+
nil,
106+
)
107+
require.NoError(t, err)
108+
109+
ctx := context.Background()
110+
testFilePath := filepath.Join(WorkDir, "test.txt")
111+
112+
cascadeMeta, price, expiration, err := actionClient.BuildCascadeMetadataFromFile(ctx, testFilePath, false, "")
113+
require.NoError(t, err)
114+
startSig, err := actionClient.GenerateStartCascadeSignatureFromFile(ctx, testFilePath)
115+
require.NoError(t, err)
116+
117+
metadataBz, err := json.Marshal(cascadeMeta)
118+
require.NoError(t, err)
119+
120+
fixtureStat, err := os.Stat(testFilePath)
121+
require.NoError(t, err)
122+
fileSizeKbs := int64(0)
123+
if fixtureStat.Size() > 0 {
124+
fileSizeKbs = (fixtureStat.Size() + 1023) / 1024
125+
}
126+
127+
reqResp, err := userLumera.ActionMsg().RequestAction(
128+
ctx,
129+
"CASCADE",
130+
string(metadataBz),
131+
price,
132+
expiration,
133+
strconv.FormatInt(fileSizeKbs, 10),
134+
)
135+
require.NoError(t, err)
136+
require.NotNil(t, reqResp)
137+
require.NotNil(t, reqResp.TxResponse)
138+
require.Zero(t, reqResp.TxResponse.Code)
139+
140+
txHash := reqResp.TxResponse.TxHash
141+
require.NotEmpty(t, txHash)
142+
sut.AwaitNextBlock(t)
143+
144+
txResp := cli.CustomQuery("q", "tx", txHash)
145+
actionID := extractActionIDFromTxQuery(txResp)
146+
require.NotEmpty(t, actionID)
147+
148+
_, err = actionClient.StartCascade(ctx, testFilePath, actionID, startSig)
149+
require.NoError(t, err)
150+
require.NoError(t, waitForActionStateWithClient(ctx, userLumera, actionID, actiontypes.ActionStateDone))
151+
152+
actionResp, err := userLumera.Action().GetAction(ctx, actionID)
153+
require.NoError(t, err)
154+
require.NotNil(t, actionResp)
155+
require.NotNil(t, actionResp.Action)
156+
cmeta, err := cascadekit.UnmarshalCascadeMetadata(actionResp.Action.Metadata)
157+
require.NoError(t, err)
158+
fileKey := pickAnchorKey(cmeta.RqIdsIds)
159+
require.NotEmpty(t, fileKey)
160+
161+
node0DiskKR, err := snkeyring.InitKeyring(snconfig.KeyringConfig{
162+
Backend: "test",
163+
Dir: filepath.Join(WorkDir, "supernode-data1", "keys"),
164+
})
165+
require.NoError(t, err)
166+
167+
node0LumeraCfg, err := lumera.NewConfig(lumeraGRPCAddr, lumeraChainID, shNode0KeyName, node0DiskKR)
168+
require.NoError(t, err)
169+
node0Lumera, err := lumera.NewClient(context.Background(), node0LumeraCfg)
170+
require.NoError(t, err)
171+
defer func() { _ = node0Lumera.Close() }()
172+
173+
shClient, err := newSelfHealingRPCClient(node0Lumera, node0DiskKR, shNode0Identity)
174+
require.NoError(t, err)
175+
176+
nodes := []selfHealingNode{
177+
{KeyName: shNode0KeyName, Identity: shNode0Identity},
178+
{KeyName: shNode1KeyName, Identity: shNode1Identity},
179+
{KeyName: shNode2KeyName, Identity: shNode2Identity},
180+
}
181+
for i := range nodes {
182+
nodes[i].GRPCAddr = mustGetSupernodeLatestAddr(t, userLumera, nodes[i].Identity)
183+
nodes[i].HasLocal = nodeHasLocalCopy(t, shClient, nodes[i], fileKey)
184+
}
185+
186+
var recipient selfHealingNode
187+
recipientForcedReconstruct := false
188+
for _, n := range nodes {
189+
if !n.HasLocal {
190+
recipient = n
191+
recipientForcedReconstruct = true
192+
break
193+
}
194+
}
195+
if recipient.Identity == "" {
196+
recipient = nodes[1]
197+
}
198+
199+
observer := recipient
200+
for _, n := range nodes {
201+
if n.Identity != recipient.Identity && n.HasLocal {
202+
observer = n
203+
break
204+
}
205+
}
206+
207+
challengeID := fmt.Sprintf("sh-e2e-happy-%d", time.Now().UnixNano())
208+
req := &pb.RequestSelfHealingRequest{
209+
ChallengeId: challengeID,
210+
EpochId: 0,
211+
FileKey: fileKey,
212+
ChallengerId: shNode0Identity,
213+
RecipientId: recipient.Identity,
214+
ObserverIds: []string{observer.Identity},
215+
}
216+
reqCtx, cancelReq := context.WithTimeout(ctx, 30*time.Second)
217+
defer cancelReq()
218+
reqRespSH, err := shClient.Request(reqCtx, recipient.Identity, recipient.GRPCAddr, req)
219+
require.NoError(t, err)
220+
require.True(t, reqRespSH.Accepted, "self-healing request was rejected: %s", reqRespSH.Error)
221+
require.NotEmpty(t, reqRespSH.ReconstructedHashHex)
222+
if recipientForcedReconstruct {
223+
require.True(t, reqRespSH.ReconstructionRequired, "expected reconstruction_required=true when recipient initially lacked local key")
224+
}
225+
226+
verifyReq := &pb.VerifySelfHealingRequest{
227+
ChallengeId: challengeID,
228+
EpochId: 0,
229+
FileKey: fileKey,
230+
RecipientId: recipient.Identity,
231+
ReconstructedHashHex: reqRespSH.ReconstructedHashHex,
232+
ObserverId: observer.Identity,
233+
}
234+
verifyCtx, cancelVerify := context.WithTimeout(ctx, 30*time.Second)
235+
defer cancelVerify()
236+
verifyResp, err := shClient.Verify(verifyCtx, observer.Identity, observer.GRPCAddr, verifyReq)
237+
require.NoError(t, err)
238+
require.True(t, verifyResp.Ok, "observer verification failed: %s", verifyResp.Error)
239+
}
240+
241+
func registerSelfHealingSupernodes(t *testing.T, cli *LumeradCli) {
242+
t.Helper()
243+
type reg struct {
244+
nodeKey string
245+
grpcPort string
246+
address string
247+
p2pPort string
248+
}
249+
nodes := []reg{
250+
{nodeKey: "node0", grpcPort: "4444", address: shNode0Identity, p2pPort: "4445"},
251+
{nodeKey: "node1", grpcPort: "4446", address: shNode1Identity, p2pPort: "4447"},
252+
{nodeKey: "node2", grpcPort: "4448", address: shNode2Identity, p2pPort: "4449"},
253+
}
254+
for _, n := range nodes {
255+
valAddr := strings.TrimSpace(cli.Keys("keys", "show", n.nodeKey, "--bech", "val", "-a"))
256+
require.NotEmpty(t, valAddr)
257+
resp := cli.CustomCommand(
258+
"tx", "supernode", "register-supernode",
259+
valAddr,
260+
"localhost:"+n.grpcPort,
261+
n.address,
262+
"--p2p-port", n.p2pPort,
263+
"--from", n.nodeKey,
264+
)
265+
RequireTxSuccess(t, resp)
266+
sut.AwaitNextBlock(t)
267+
}
268+
}
269+
270+
func extractActionIDFromTxQuery(txResp string) string {
271+
events := gjson.Get(txResp, "events").Array()
272+
for _, event := range events {
273+
if event.Get("type").String() != "action_registered" {
274+
continue
275+
}
276+
attrs := event.Get("attributes").Array()
277+
for _, attr := range attrs {
278+
if attr.Get("key").String() == "action_id" {
279+
return attr.Get("value").String()
280+
}
281+
}
282+
}
283+
return ""
284+
}
285+
286+
func mustGetSupernodeLatestAddr(t *testing.T, client lumera.Client, identity string) string {
287+
t.Helper()
288+
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
289+
defer cancel()
290+
info, err := client.SuperNode().GetSupernodeWithLatestAddress(ctx, identity)
291+
require.NoError(t, err)
292+
require.NotNil(t, info)
293+
addr := strings.TrimSpace(info.LatestAddress)
294+
require.NotEmpty(t, addr)
295+
return addr
296+
}
297+
298+
func nodeHasLocalCopy(t *testing.T, shClient *selfHealingRPCClient, node selfHealingNode, fileKey string) bool {
299+
t.Helper()
300+
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
301+
defer cancel()
302+
probeReq := &pb.VerifySelfHealingRequest{
303+
ChallengeId: fmt.Sprintf("sh-probe-%s-%d", node.KeyName, time.Now().UnixNano()),
304+
EpochId: 0,
305+
FileKey: fileKey,
306+
RecipientId: "",
307+
ReconstructedHashHex: strings.Repeat("0", 64),
308+
ObserverId: node.Identity,
309+
}
310+
resp, err := shClient.Verify(ctx, node.Identity, node.GRPCAddr, probeReq)
311+
require.NoError(t, err)
312+
if resp == nil {
313+
return false
314+
}
315+
if strings.Contains(strings.ToLower(resp.Error), "missing local file") {
316+
return false
317+
}
318+
return true
319+
}
320+
321+
func pickAnchorKey(keys []string) string {
322+
anchor := ""
323+
for _, raw := range keys {
324+
key := strings.TrimSpace(raw)
325+
if key == "" {
326+
continue
327+
}
328+
if anchor == "" || key < anchor {
329+
anchor = key
330+
}
331+
}
332+
return anchor
333+
}
334+
335+
func newSelfHealingRPCClient(lumeraClient lumera.Client, kr keyring.Keyring, localIdentity string) (*selfHealingRPCClient, error) {
336+
validator := lumera.NewSecureKeyExchangeValidator(lumeraClient)
337+
grpcCreds, err := credentials.NewClientCreds(&credentials.ClientOptions{
338+
CommonOptions: credentials.CommonOptions{
339+
Keyring: kr,
340+
LocalIdentity: localIdentity,
341+
PeerType: securekeyx.Supernode,
342+
Validator: validator,
343+
},
344+
})
345+
if err != nil {
346+
return nil, err
347+
}
348+
opts := grpcclient.DefaultClientOptions()
349+
opts.EnableRetries = true
350+
return &selfHealingRPCClient{
351+
client: grpcclient.NewClient(grpcCreds),
352+
opts: opts,
353+
}, nil
354+
}
355+
356+
func (c *selfHealingRPCClient) Request(ctx context.Context, remoteIdentity string, address string, req *pb.RequestSelfHealingRequest) (*pb.RequestSelfHealingResponse, error) {
357+
conn, err := c.client.Connect(ctx, fmt.Sprintf("%s@%s", strings.TrimSpace(remoteIdentity), address), c.opts)
358+
if err != nil {
359+
return nil, err
360+
}
361+
defer conn.Close()
362+
return pb.NewSelfHealingServiceClient(conn).RequestSelfHealing(ctx, req)
363+
}
364+
365+
func (c *selfHealingRPCClient) Verify(ctx context.Context, remoteIdentity string, address string, req *pb.VerifySelfHealingRequest) (*pb.VerifySelfHealingResponse, error) {
366+
conn, err := c.client.Connect(ctx, fmt.Sprintf("%s@%s", strings.TrimSpace(remoteIdentity), address), c.opts)
367+
if err != nil {
368+
return nil, err
369+
}
370+
defer conn.Close()
371+
return pb.NewSelfHealingServiceClient(conn).VerifySelfHealing(ctx, req)
372+
}

0 commit comments

Comments
 (0)