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  }