github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/systests/stellar_test.go (about) 1 package systests 2 3 import ( 4 "bytes" 5 "net/http" 6 "strings" 7 "testing" 8 "time" 9 10 "golang.org/x/net/context" 11 12 "github.com/keybase/client/go/client" 13 "github.com/keybase/client/go/kbtest" 14 "github.com/keybase/client/go/libkb" 15 keybase1 "github.com/keybase/client/go/protocol/keybase1" 16 "github.com/keybase/client/go/protocol/stellar1" 17 "github.com/keybase/client/go/stellar" 18 "github.com/keybase/client/go/teams" 19 "github.com/keybase/stellarnet" 20 "github.com/stellar/go/build" 21 // nolint 22 "github.com/stellar/go/clients/horizon" 23 "github.com/stretchr/testify/require" 24 ) 25 26 const disable = true 27 const disableMsg = "new protocol version on testnet incompatible with stellard" 28 29 func TestStellarNoteRoundtripAndResets(t *testing.T) { 30 if disable { 31 t.Skip(disableMsg) 32 } 33 ctx := newSMUContext(t) 34 defer ctx.cleanup() 35 36 // Sign up two users, bob and alice. 37 alice := ctx.installKeybaseForUser("alice", 10) 38 alice.signup() 39 divDebug(ctx, "Signed up alice (%s)", alice.username) 40 bob := ctx.installKeybaseForUser("bob", 10) 41 bob.signup() 42 divDebug(ctx, "Signed up bob (%s)", bob.username) 43 44 t.Logf("note to self") 45 encB64, err := stellar.NoteEncryptB64(libkb.NewMetaContextBackground(alice.getPrimaryGlobalContext()), sampleNote(), nil) 46 require.NoError(t, err) 47 note, err := stellar.NoteDecryptB64(libkb.NewMetaContextBackground(alice.getPrimaryGlobalContext()), encB64) 48 require.NoError(t, err) 49 require.Equal(t, sampleNote(), note) 50 51 t.Logf("note to both users") 52 other := bob.userVersion() 53 encB64, err = stellar.NoteEncryptB64(libkb.NewMetaContextBackground(alice.getPrimaryGlobalContext()), sampleNote(), &other) 54 require.NoError(t, err) 55 56 t.Logf("decrypt as self") 57 note, err = stellar.NoteDecryptB64(libkb.NewMetaContextBackground(alice.getPrimaryGlobalContext()), encB64) 58 require.NoError(t, err) 59 require.Equal(t, sampleNote(), note) 60 61 t.Logf("decrypt as other") 62 note, err = stellar.NoteDecryptB64(libkb.NewMetaContextBackground(bob.getPrimaryGlobalContext()), encB64) 63 require.NoError(t, err) 64 require.Equal(t, sampleNote(), note) 65 66 t.Logf("reset sender") 67 alice.reset() 68 divDebug(ctx, "Reset bob (%s)", bob.username) 69 alice.loginAfterReset(10) 70 divDebug(ctx, "Bob logged in after reset") 71 72 t.Logf("fail to decrypt as post-reset self") 73 _, err = stellar.NoteDecryptB64(libkb.NewMetaContextBackground(alice.getPrimaryGlobalContext()), encB64) 74 require.Error(t, err) 75 require.Equal(t, "note not encrypted for logged-in user", err.Error()) 76 77 t.Logf("decrypt as other") 78 note, err = stellar.NoteDecryptB64(libkb.NewMetaContextBackground(bob.getPrimaryGlobalContext()), encB64) 79 require.NoError(t, err) 80 require.Equal(t, sampleNote(), note) 81 } 82 83 // Test took 38s on a dev server 2018-06-07 84 func TestStellarRelayAutoClaims(t *testing.T) { 85 kbtest.SkipTestOnNonMasterCI(t, "slow stellar test") 86 if disable { 87 t.Skip(disableMsg) 88 } 89 testStellarRelayAutoClaims(t, false, false) 90 } 91 92 // Test took 29s on a dev server 2018-06-07 93 func TestStellarRelayAutoClaimsWithPUK(t *testing.T) { 94 kbtest.SkipTestOnNonMasterCI(t, "slow stellar test") 95 if disable { 96 t.Skip(disableMsg) 97 } 98 testStellarRelayAutoClaims(t, true, true) 99 } 100 101 // Part 1: 102 // XLM is sent to a user before they have a [PUK / wallet]. 103 // In the form of multiple relay payments. 104 // They then [get a PUK,] add a wallet, and enter the impteam, 105 // which all kick the autoclaim into gear. 106 // 107 // Part 2: 108 // A relay payment is sent to the user who already has a wallet. 109 // The funds should be claimed asap. 110 // 111 // To debug this test use log filter "stellar_test|poll-|AutoClaim|stellar.claim|pollfor" 112 func testStellarRelayAutoClaims(t *testing.T, startWithPUK, skipPart2 bool) { 113 tt := newTeamTester(t) 114 defer tt.cleanup() 115 useStellarTestNet(t) 116 117 alice := tt.addUser("alice") 118 var bob *userPlusDevice 119 if startWithPUK { 120 bob = tt.addUser("bob") 121 } else { 122 bob = tt.addPuklessUser("bob") 123 } 124 alice.kickTeamRekeyd() 125 126 t.Logf("alice gets funded") 127 acceptDisclaimer(alice) 128 129 baseFeeStroops := int64(alice.tc.G.GetStellar().(*stellar.Stellar).WalletStateForTest().BaseFee(alice.tc.MetaContext())) 130 131 res, err := alice.stellarClient.GetWalletAccountsLocal(context.Background(), 0) 132 require.NoError(t, err) 133 gift(t, res[0].AccountID) 134 135 t.Logf("alice sends a first relay payment to bob P1") 136 attachIdentifyUI(t, alice.tc.G, newSimpleIdentifyUI()) 137 cmd := client.CmdWalletSend{ 138 Contextified: libkb.NewContextified(alice.tc.G), 139 Recipient: bob.username, 140 Amount: "50", 141 } 142 for i := 0; i < retryCount; i++ { 143 err = cmd.Run() 144 if err == nil { 145 break 146 } 147 } 148 require.NoError(t, err) 149 150 t.Logf("alice sends a second relay payment to bob P2") 151 cmd = client.CmdWalletSend{ 152 Contextified: libkb.NewContextified(alice.tc.G), 153 Recipient: bob.username, 154 Amount: "30", 155 } 156 for i := 0; i < retryCount; i++ { 157 err = cmd.Run() 158 if err == nil { 159 break 160 } 161 } 162 require.NoError(t, err) 163 164 t.Logf("get the impteam seqno to wait on later") 165 team, _, _, err := teams.LookupImplicitTeam(context.Background(), alice.tc.G, alice.username+","+bob.username, false, teams.ImplicitTeamOptions{}) 166 require.NoError(t, err) 167 nextSeqno := team.NextSeqno() 168 169 if startWithPUK { 170 t.Logf("bob gets a wallet") 171 acceptDisclaimer(bob) 172 } else { 173 t.Logf("bob gets a PUK and wallet") 174 bob.device.tctx.Tp.DisableUpgradePerUserKey = false 175 acceptDisclaimer(bob) 176 177 t.Logf("wait for alice to add bob to their impteam") 178 alice.pollForTeamSeqnoLinkWithLoadArgs(keybase1.LoadTeamArg{ID: team.ID}, nextSeqno) 179 } 180 181 pollTime := 20 * time.Second 182 if libkb.UseCITime(bob.tc.G) { 183 // This test is especially slow because it's waiting on multiple transactions 184 pollTime = 90 * time.Second 185 } 186 187 pollFor(t, "claims to complete", pollTime, bob.tc.G, func(i int) bool { 188 // The first claims takes a create_account + account_merge. The second only account_merge. 189 res, err = bob.stellarClient.GetWalletAccountsLocal(context.Background(), 0) 190 require.NoError(t, err) 191 t.Logf("poll-1-%v: %v", i, res[0].BalanceDescription) 192 if res[0].BalanceDescription == "0 XLM" { 193 return false 194 } 195 if isWithinFeeBounds(t, res[0].BalanceDescription, "50", baseFeeStroops*2) { 196 t.Logf("poll-1-%v: received T1 but not T2", i) 197 return false 198 } 199 if isWithinFeeBounds(t, res[0].BalanceDescription, "30", baseFeeStroops*2) { 200 t.Logf("poll-1-%v: received T2 but not T1", i) 201 return false 202 } 203 t.Logf("poll-1-%v: received both payments", i) 204 assertWithinFeeBounds(t, res[0].BalanceDescription, "80", baseFeeStroops*3) 205 return true 206 }) 207 208 if skipPart2 { 209 t.Logf("Skipping part 2") 210 return 211 } 212 213 t.Logf("--------------------") 214 t.Logf("Part 2: Alice sends a relay payment to bob who now already has a wallet") 215 cmd = client.CmdWalletSend{ 216 Contextified: libkb.NewContextified(alice.tc.G), 217 Recipient: bob.username, 218 Amount: "10", 219 ForceRelay: true, 220 } 221 for i := 0; i < retryCount; i++ { 222 err = cmd.Run() 223 if err == nil { 224 break 225 } 226 } 227 require.NoError(t, err) 228 229 pollFor(t, "final claim to complete", pollTime, bob.tc.G, func(i int) bool { 230 res, err = bob.stellarClient.GetWalletAccountsLocal(context.Background(), 0) 231 require.NoError(t, err) 232 t.Logf("poll-2-%v: %v", i, res[0].BalanceDescription) 233 if isWithinFeeBounds(t, res[0].BalanceDescription, "80", baseFeeStroops*3) { 234 return false 235 } 236 t.Logf("poll-1-%v: received final payment", i) 237 assertWithinFeeBounds(t, res[0].BalanceDescription, "90", baseFeeStroops*4) 238 return true 239 }) 240 241 } 242 243 // XLM is sent to a rooter assertion that does not resolve. 244 // The recipient-to-be signs up, gets a wallet, and then proves the assertion. 245 // The recipient enters the impteam which kicks autoclaim into gear. 246 // 247 // To debug this test use log filter "stellar_test|poll-|AutoClaim|stellar.claim|pollfor" 248 // Test took 20s on a dev server 2019-01-23 249 func TestStellarRelayAutoClaimsSBS(t *testing.T) { 250 kbtest.SkipTestOnNonMasterCI(t, "slow stellar test") 251 if disable { 252 t.Skip(disableMsg) 253 } 254 tt := newTeamTester(t) 255 defer tt.cleanup() 256 useStellarTestNet(t) 257 258 alice := tt.addUser("alice") 259 bob := tt.addUser("bob") 260 rooterAssertion := bob.username + "@rooter" 261 alice.kickTeamRekeyd() 262 263 t.Logf("alice gets funded") 264 acceptDisclaimer(alice) 265 266 res, err := alice.stellarClient.GetWalletAccountsLocal(context.Background(), 0) 267 require.NoError(t, err) 268 gift(t, res[0].AccountID) 269 270 t.Logf("alice sends a first relay payment to bob P1") 271 attachIdentifyUI(t, alice.tc.G, newSimpleIdentifyUI()) 272 cmd := client.CmdWalletSend{ 273 Contextified: libkb.NewContextified(alice.tc.G), 274 Recipient: rooterAssertion, 275 Amount: "50", 276 } 277 for i := 0; i < retryCount; i++ { 278 err = cmd.Run() 279 if err == nil { 280 break 281 } 282 } 283 require.NoError(t, err) 284 baseFeeStroops := int64(alice.tc.G.GetStellar().(*stellar.Stellar).WalletStateForTest().BaseFee(alice.tc.MetaContext())) 285 t.Logf("baseFeeStroops %v", baseFeeStroops) 286 287 t.Logf("get the impteam seqno to wait on later") 288 team, _, _, err := teams.LookupImplicitTeam(context.Background(), alice.tc.G, alice.username+","+rooterAssertion, false, teams.ImplicitTeamOptions{}) 289 require.NoError(t, err) 290 nextSeqno := team.NextSeqno() 291 292 t.Logf("bob proves his rooter") 293 tt.users[1].proveRooter() 294 t.Logf("bob gets a wallet") 295 acceptDisclaimer(bob) 296 297 t.Logf("wait for alice to add bob to their impteam") 298 alice.pollForTeamSeqnoLinkWithLoadArgs(keybase1.LoadTeamArg{ID: team.ID}, nextSeqno) 299 300 pollTime := 20 * time.Second 301 if libkb.UseCITime(bob.tc.G) { 302 // This test is especially slow. 303 pollTime = 30 * time.Second 304 } 305 306 pollFor(t, "claim to complete", pollTime, bob.tc.G, func(i int) bool { 307 res, err = bob.stellarClient.GetWalletAccountsLocal(context.Background(), 0) 308 require.NoError(t, err) 309 t.Logf("poll-1-%v: %v", i, res[0].BalanceDescription) 310 if res[0].BalanceDescription == "0 XLM" { 311 return false 312 } 313 t.Logf("poll-1-%v: received P1", i) 314 require.NoError(t, err) 315 // This assertion could potentially fail if baseFee changes between the send and BaseFee calls above. 316 assertWithinFeeBounds(t, res[0].BalanceDescription, "50", baseFeeStroops*2) // paying for create_account + account_merge 317 return true 318 }) 319 } 320 321 // Assert that: target - maxMissingStroops <= amount <= target 322 // Strips suffix off amount. 323 func assertWithinFeeBounds(t testing.TB, amount string, target string, maxFeeStroops int64) { 324 suffix := " XLM" 325 amount = strings.TrimSuffix(amount, suffix) 326 amountX, err := stellarnet.ParseStellarAmount(amount) 327 require.NoError(t, err) 328 targetX, err := stellarnet.ParseStellarAmount(target) 329 require.NoError(t, err) 330 lowestX := targetX - maxFeeStroops 331 require.LessOrEqual(t, amountX, targetX) 332 require.LessOrEqual(t, lowestX, amountX) 333 } 334 335 func isWithinFeeBounds(t testing.TB, amount string, target string, maxFeeStroops int64) bool { 336 suffix := " XLM" 337 amount = strings.TrimSuffix(amount, suffix) 338 amountX, err := stellarnet.ParseStellarAmount(amount) 339 require.NoError(t, err) 340 targetX, err := stellarnet.ParseStellarAmount(target) 341 require.NoError(t, err) 342 lowestX := targetX - maxFeeStroops 343 return amountX <= targetX && amountX >= lowestX 344 } 345 346 func sampleNote() stellar1.NoteContents { 347 return stellar1.NoteContents{ 348 Note: "wizbang", 349 StellarID: stellar1.TransactionID("6653fc2fdbc42ad51ccbe77ee0a3c29e258a5513c62fdc532cbfff91ab101abf"), 350 } 351 } 352 353 // Friendbot sends someone XLM 354 func gift(t testing.TB, accountID stellar1.AccountID) { 355 t.Logf("gift -> %v", accountID) 356 url := "https://friendbot.stellar.org/?addr=" + accountID.String() 357 for i := 0; i < retryCount; i++ { 358 t.Logf("gift url: %v", url) 359 res, err := http.Get(url) 360 if err != nil { 361 t.Logf("http get %s error: %s", url, err) 362 continue 363 } 364 bodyBuf := new(bytes.Buffer) 365 _, err = bodyBuf.ReadFrom(res.Body) 366 require.NoError(t, err) 367 res.Body.Close() 368 t.Logf("gift res: %v", bodyBuf.String()) 369 if res.StatusCode == 200 { 370 return 371 } 372 t.Logf("gift status not ok: %d", res.StatusCode) 373 } 374 t.Fatalf("gift to %s failed after multiple attempts", accountID) 375 } 376 377 func useStellarTestNet(t testing.TB) { 378 stellarnet.SetClientAndNetwork(horizon.DefaultTestNetClient, build.TestNetwork) 379 } 380 381 func acceptDisclaimer(u *userPlusDevice) { 382 err := u.stellarClient.AcceptDisclaimerLocal(context.Background(), 0) 383 require.NoError(u.tc.T, err) 384 } 385 386 func TestAccountMerge(t *testing.T) { 387 if disable { 388 t.Skip(disableMsg) 389 } 390 tt := newTeamTester(t) 391 defer tt.cleanup() 392 useStellarTestNet(t) 393 ctx := context.Background() 394 alice := tt.addUser("alice") 395 396 t.Logf("fund two accounts for alice from one friendbot gift for 10k lumens") 397 acceptDisclaimer(alice) 398 walletState := alice.tc.G.GetStellar().(*stellar.Stellar).WalletStateForTest() 399 getRes, err := alice.stellarClient.GetWalletAccountsLocal(context.Background(), 0) 400 firstAccountID := getRes[0].AccountID 401 require.NoError(t, err) 402 secondAccountID, err := alice.stellarClient.CreateWalletAccountLocal(ctx, stellar1.CreateWalletAccountLocalArg{Name: "second"}) 403 require.NoError(t, err) 404 405 stroopsInAcct := func(acctID stellar1.AccountID) int64 { 406 acctBalances, err := walletState.Balances(ctx, acctID) 407 require.NoError(t, err) 408 if len(acctBalances) == 0 { 409 return 0 410 } 411 amount, err := stellarnet.ParseStellarAmount(acctBalances[0].Amount) 412 require.NoError(t, err) 413 return amount 414 } 415 416 pollTime := 20 * time.Second 417 if libkb.UseCITime(alice.tc.G) { 418 // This test is especially slow. 419 pollTime = 30 * time.Second 420 } 421 422 gift(t, firstAccountID) 423 pollFor(t, "set up first account", pollTime, alice.tc.G, func(i int) bool { 424 err = walletState.Refresh(alice.tc.MetaContext(), firstAccountID, "test") 425 require.NoError(t, err) 426 return stroopsInAcct(firstAccountID) > 0 427 }) 428 429 attachIdentifyUI(t, alice.tc.G, newSimpleIdentifyUI()) 430 sendCmd := client.CmdWalletSend{ 431 Contextified: libkb.NewContextified(alice.tc.G), 432 Recipient: secondAccountID.String(), 433 Amount: "50", 434 } 435 for i := 0; i < retryCount; i++ { 436 err = sendCmd.Run() 437 if err == nil { 438 break 439 } 440 } 441 require.NoError(t, err) 442 443 pollFor(t, "set up second account", pollTime, alice.tc.G, func(i int) bool { 444 err = walletState.Refresh(alice.tc.MetaContext(), firstAccountID, "test") 445 require.NoError(t, err) 446 err = walletState.Refresh(alice.tc.MetaContext(), secondAccountID, "test") 447 require.NoError(t, err) 448 secondAcctBalance := stroopsInAcct(secondAccountID) 449 if secondAcctBalance == 0 { 450 t.Logf("waiting on payment between accounts to complete") 451 return false 452 } 453 require.Equal(t, secondAcctBalance, int64(50*stellarnet.StroopsPerLumen)) 454 return true 455 }) 456 t.Logf("10k lumens split into two accounts: ~99,949.999 and 50") 457 458 beforeMergeBalance := stroopsInAcct(firstAccountID) 459 mergeCmd := client.CmdWalletMerge{ 460 Contextified: libkb.NewContextified(alice.tc.G), 461 FromAccountID: secondAccountID, 462 To: firstAccountID.String(), 463 } 464 err = mergeCmd.Run() 465 require.NoError(t, err) 466 467 pollFor(t, "merge command", pollTime, alice.tc.G, func(i int) bool { 468 err = walletState.RefreshAll(alice.tc.MetaContext(), "test") 469 require.NoError(t, err) 470 afterMergeBalance := stroopsInAcct(firstAccountID) 471 if beforeMergeBalance == afterMergeBalance { 472 t.Logf("waiting on merge to complete") 473 return false 474 } 475 return true 476 }) 477 478 t.Logf("merged the second into the first") 479 afterMergeBalance := stroopsInAcct(firstAccountID) 480 lowerBoundFinalExpectedAmount := int64(stellarnet.StroopsPerLumen * 9999.99) 481 require.True(t, afterMergeBalance > lowerBoundFinalExpectedAmount) 482 t.Logf("value of the second account was merged into the first account") 483 }