github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/teams/hidden_loader_test.go (about)

     1  package teams
     2  
     3  import (
     4  	"testing"
     5  
     6  	"github.com/keybase/client/go/engine"
     7  	"github.com/keybase/client/go/teams/hidden"
     8  
     9  	"github.com/keybase/clockwork"
    10  	"golang.org/x/net/context"
    11  
    12  	"github.com/keybase/client/go/libkb"
    13  	"github.com/keybase/client/go/protocol/keybase1"
    14  	jsonw "github.com/keybase/go-jsonw"
    15  	"github.com/stretchr/testify/require"
    16  )
    17  
    18  func makeHiddenRotation(t *testing.T, userContext *libkb.GlobalContext, teamName keybase1.TeamName) {
    19  	team, err := GetForTestByStringName(context.TODO(), userContext, teamName.String())
    20  	require.NoError(t, err)
    21  	err = team.Rotate(context.TODO(), keybase1.RotationType_HIDDEN)
    22  	require.NoError(t, err)
    23  }
    24  
    25  func loadTeamAndAssertCommittedAndUncommittedSeqnos(t *testing.T, tc *libkb.TestContext, teamID keybase1.TeamID, uncommittedSeqno keybase1.Seqno) {
    26  	// since polling does not take into account hidden chain updates, manually update the merkle root.
    27  	_, err := tc.G.GetMerkleClient().FetchRootFromServer(libkb.NewMetaContextForTest(*tc), 0)
    28  	require.NoError(t, err)
    29  	_, teamHiddenChain, err := tc.G.GetTeamLoader().Load(context.TODO(), keybase1.LoadTeamArg{
    30  		ID:          teamID,
    31  		ForceRepoll: true,
    32  	})
    33  	require.NoError(t, err)
    34  	require.Equal(t, uncommittedSeqno, teamHiddenChain.Last, "committed seqno")
    35  }
    36  
    37  func assertHiddenMerkleErrorType(t *testing.T, err error, expType libkb.HiddenMerkleErrorType) {
    38  	require.Error(t, err)
    39  	require.IsType(t, libkb.HiddenMerkleError{}, err)
    40  	require.Equal(t, err.(libkb.HiddenMerkleError).ErrorType(), expType)
    41  }
    42  
    43  func loadTeamAndCheckCommittedAndUncommittedSeqnos(t *testing.T, tc *libkb.TestContext, teamID keybase1.TeamID, uncommittedSeqno keybase1.Seqno) bool {
    44  	_, teamHiddenChain, err := tc.G.GetTeamLoader().Load(context.TODO(), keybase1.LoadTeamArg{
    45  		ID:          teamID,
    46  		ForceRepoll: true,
    47  	})
    48  	require.NoError(t, err)
    49  	if uncommittedSeqno != teamHiddenChain.Last {
    50  		t.Logf("Error: uncommittedSeqno != teamHiddenChain.Last: %v != %v ", uncommittedSeqno, teamHiddenChain.Last)
    51  		return false
    52  	}
    53  	return true
    54  }
    55  
    56  func loadTeamAndAssertUncommittedSeqno(t *testing.T, tc *libkb.TestContext, teamID keybase1.TeamID, uncommittedSeqno keybase1.Seqno) {
    57  	_, teamHiddenChain, err := tc.G.GetTeamLoader().Load(context.TODO(), keybase1.LoadTeamArg{
    58  		ID:          teamID,
    59  		ForceRepoll: true,
    60  	})
    61  	require.NoError(t, err)
    62  	require.Equal(t, uncommittedSeqno, teamHiddenChain.Last)
    63  }
    64  
    65  func loadTeamAndAssertNoHiddenChainExists(t *testing.T, tc *libkb.TestContext, teamID keybase1.TeamID) {
    66  	teamChain, teamHiddenChain, err := tc.G.GetTeamLoader().Load(context.TODO(), keybase1.LoadTeamArg{
    67  		ID:          teamID,
    68  		ForceRepoll: true,
    69  	})
    70  	require.NoError(t, err)
    71  	require.NotNil(t, teamChain)
    72  	require.Nil(t, teamHiddenChain)
    73  }
    74  
    75  func getCurrentBlindRootHashFromMerkleRoot(t *testing.T, tc libkb.TestContext) string {
    76  	apiRes, err := tc.G.API.Get(libkb.NewMetaContextForTest(tc), libkb.APIArg{
    77  		Endpoint: "merkle/root",
    78  	})
    79  	require.NoError(t, err)
    80  	payloadStr, err := apiRes.Body.AtKey("payload_json").GetString()
    81  	require.NoError(t, err)
    82  	payload, err := jsonw.Unmarshal([]byte(payloadStr))
    83  	require.NoError(t, err)
    84  	blindRoot, err := payload.AtKey("body").AtKey("blind_merkle_root_hash").GetString()
    85  	require.NoError(t, err)
    86  
    87  	return blindRoot
    88  }
    89  
    90  func makePaperKey(t *testing.T, uTc *libkb.TestContext) {
    91  	uis := libkb.UIs{
    92  		LogUI:    uTc.G.UI.GetLogUI(),
    93  		LoginUI:  &libkb.TestLoginUI{},
    94  		SecretUI: &libkb.TestSecretUI{},
    95  	}
    96  	eng := engine.NewPaperKey(uTc.G)
    97  	err := engine.RunEngine2(libkb.NewMetaContextForTest(*uTc).WithUIs(uis), eng)
    98  	require.NoError(t, err)
    99  }
   100  
   101  func requestNewBlindTreeFromArchitectAndWaitUntilDone(t *testing.T, uTc *libkb.TestContext) {
   102  	oldBlindRoot := getCurrentBlindRootHashFromMerkleRoot(t, *uTc)
   103  
   104  	// make the architect run. This returns when the architect has finished a round
   105  	_, err := uTc.G.API.Get(libkb.NewMetaContextForTest(*uTc), libkb.APIArg{
   106  		Endpoint: "test/build_blind_tree",
   107  	})
   108  	require.NoError(t, err)
   109  
   110  	// the user adds a paper key to make new main merkle tree version.
   111  	makePaperKey(t, uTc)
   112  
   113  	// ensure the architect actually updated
   114  	newBlindRoot := getCurrentBlindRootHashFromMerkleRoot(t, *uTc)
   115  	require.NotEqual(t, oldBlindRoot, newBlindRoot)
   116  }
   117  
   118  func retryTestNTimes(t *testing.T, n int, f func(t *testing.T) bool) {
   119  	for i := 0; i < n; i++ {
   120  		succeeded := f(t)
   121  		if succeeded {
   122  			t.Logf("Succeeded!")
   123  			return
   124  		}
   125  	}
   126  	t.Errorf("Test did not succeed any of the %v times", n)
   127  }
   128  func TestHiddenLoadSucceedsIfServerDoesntCommitLinks(t *testing.T) {
   129  	retryTestNTimes(t, 5, testHiddenLoadSucceedsIfServerDoesntCommitLinks)
   130  }
   131  
   132  func testHiddenLoadSucceedsIfServerDoesntCommitLinks(t *testing.T) bool {
   133  	fus, tcs, cleanup := setupNTests(t, 2)
   134  	defer cleanup()
   135  
   136  	clock := clockwork.NewFakeClock()
   137  	tcs[1].G.SetClock(clock)
   138  
   139  	t.Logf("create team")
   140  	teamName, teamID := createTeam2(*tcs[0])
   141  
   142  	t.Logf("add B to the team so they can load it")
   143  	_, err := AddMember(context.TODO(), tcs[0].G, teamName.String(), fus[1].Username, keybase1.TeamRole_WRITER, nil)
   144  	require.NoError(t, err)
   145  
   146  	// There have been no hidden rotations yet.
   147  	loadTeamAndAssertNoHiddenChainExists(t, tcs[1], teamID)
   148  
   149  	makeHiddenRotation(t, tcs[0].G, teamName)
   150  
   151  	loadTeamAndAssertUncommittedSeqno(t, tcs[1], teamID, 1)
   152  
   153  	// make the architect run
   154  	requestNewBlindTreeFromArchitectAndWaitUntilDone(t, tcs[0])
   155  
   156  	loadTeamAndAssertCommittedAndUncommittedSeqnos(t, tcs[1], teamID, 1)
   157  
   158  	// make another hidden rotation
   159  	makeHiddenRotation(t, tcs[0].G, teamName)
   160  
   161  	// This has the potential to flake, if the architect runs concurrently and does make a new blind tree version.
   162  	if !loadTeamAndCheckCommittedAndUncommittedSeqnos(t, tcs[1], teamID, 2) {
   163  		return false
   164  	}
   165  
   166  	// now, move the clock forward and reload. The hidden loader should complain about seqno 2 not being committed
   167  	clock.Advance(2 * hidden.MaxDelayInCommittingHiddenLinks)
   168  	tcs[1].G.SetClock(clock)
   169  	_, _, err = tcs[1].G.GetTeamLoader().Load(context.TODO(), keybase1.LoadTeamArg{
   170  		ID:          teamID,
   171  		ForceRepoll: true,
   172  	})
   173  
   174  	return err == nil
   175  }
   176  
   177  type CorruptingMockLoaderContext struct {
   178  	LoaderContext
   179  
   180  	merkleCorruptorFunc func(r1 keybase1.Seqno, r2 keybase1.LinkID, hiddenResp *libkb.MerkleHiddenResponse, lastMerkleRoot *libkb.MerkleRoot, err error) (keybase1.Seqno, keybase1.LinkID, *libkb.MerkleHiddenResponse, *libkb.MerkleRoot, error)
   181  }
   182  
   183  func (c CorruptingMockLoaderContext) merkleLookupWithHidden(ctx context.Context, teamID keybase1.TeamID, public bool) (r1 keybase1.Seqno, r2 keybase1.LinkID, hiddenResp *libkb.MerkleHiddenResponse, lastMerkleRoot *libkb.MerkleRoot, err error) {
   184  	return c.merkleCorruptorFunc(c.LoaderContext.merkleLookupWithHidden(ctx, teamID, public))
   185  }
   186  
   187  var _ LoaderContext = CorruptingMockLoaderContext{}
   188  
   189  func TestHiddenLoadFailsIfServerRollsbackUncommittedSeqno(t *testing.T) {
   190  	fus, tcs, cleanup := setupNTests(t, 2)
   191  	defer cleanup()
   192  
   193  	t.Logf("create team")
   194  	teamName, teamID := createTeam2(*tcs[0])
   195  
   196  	t.Logf("add B to the team so they can load it")
   197  	_, err := AddMember(context.TODO(), tcs[0].G, teamName.String(), fus[1].Username, keybase1.TeamRole_WRITER, nil)
   198  	require.NoError(t, err)
   199  
   200  	// There have been no hidden rotations yet.
   201  	loadTeamAndAssertNoHiddenChainExists(t, tcs[1], teamID)
   202  
   203  	makeHiddenRotation(t, tcs[0].G, teamName)
   204  
   205  	loadTeamAndAssertUncommittedSeqno(t, tcs[1], teamID, 1)
   206  
   207  	// now load the team again, but this time we change the response of the server to rollback the number of committed sequence numbers
   208  	newLoader := tcs[1].G.GetTeamLoader()
   209  	newLoader.(*TeamLoader).world = CorruptingMockLoaderContext{
   210  		LoaderContext: newLoader.(*TeamLoader).world,
   211  		merkleCorruptorFunc: func(r1 keybase1.Seqno, r2 keybase1.LinkID, hiddenResp *libkb.MerkleHiddenResponse, lastMerkleRoot *libkb.MerkleRoot, err error) (keybase1.Seqno, keybase1.LinkID, *libkb.MerkleHiddenResponse, *libkb.MerkleRoot, error) {
   212  			if hiddenResp != nil && hiddenResp.UncommittedSeqno >= 1 {
   213  				hiddenResp.UncommittedSeqno--
   214  				t.Logf("Simulating malicious server: updating hiddenResp.UncommittedSeqno (new value %v)", hiddenResp.UncommittedSeqno)
   215  			}
   216  			return r1, r2, hiddenResp, lastMerkleRoot, err
   217  		},
   218  	}
   219  	tcs[1].G.SetTeamLoader(newLoader)
   220  
   221  	_, _, err = tcs[1].G.GetTeamLoader().Load(context.TODO(), keybase1.LoadTeamArg{
   222  		ID:          teamID,
   223  		ForceRepoll: true,
   224  	})
   225  	assertHiddenMerkleErrorType(t, err, libkb.HiddenMerkleErrorRollbackUncommittedSeqno)
   226  }
   227  
   228  func TestHiddenLoadFailsIfServerDoesNotReturnPromisedLinks(t *testing.T) {
   229  	fus, tcs, cleanup := setupNTests(t, 2)
   230  	defer cleanup()
   231  
   232  	t.Logf("create team")
   233  	teamName, teamID := createTeam2(*tcs[0])
   234  
   235  	t.Logf("add B to the team so they can load it")
   236  	_, err := AddMember(context.TODO(), tcs[0].G, teamName.String(), fus[1].Username, keybase1.TeamRole_WRITER, nil)
   237  	require.NoError(t, err)
   238  
   239  	// There have been no hidden rotations yet.
   240  	loadTeamAndAssertNoHiddenChainExists(t, tcs[1], teamID)
   241  
   242  	makeHiddenRotation(t, tcs[0].G, teamName)
   243  
   244  	loadTeamAndAssertUncommittedSeqno(t, tcs[1], teamID, 1)
   245  
   246  	// now load the team again, but this time we change the response of the server as if there were more hidden links
   247  	newLoader := tcs[1].G.GetTeamLoader()
   248  	newLoader.(*TeamLoader).world = CorruptingMockLoaderContext{
   249  		LoaderContext: newLoader.(*TeamLoader).world,
   250  		merkleCorruptorFunc: func(r1 keybase1.Seqno, r2 keybase1.LinkID, hiddenResp *libkb.MerkleHiddenResponse, lastMerkleRoot *libkb.MerkleRoot, err error) (keybase1.Seqno, keybase1.LinkID, *libkb.MerkleHiddenResponse, *libkb.MerkleRoot, error) {
   251  			if hiddenResp != nil && hiddenResp.UncommittedSeqno >= 1 {
   252  				hiddenResp.UncommittedSeqno += 5
   253  				t.Logf("Simulating malicious server: updating hiddenResp.UncommittedSeqno (new value %v)", hiddenResp.UncommittedSeqno)
   254  			}
   255  			return r1, r2, hiddenResp, lastMerkleRoot, err
   256  		},
   257  	}
   258  	tcs[1].G.SetTeamLoader(newLoader)
   259  
   260  	_, _, err = tcs[1].G.GetTeamLoader().Load(context.TODO(), keybase1.LoadTeamArg{
   261  		ID:          teamID,
   262  		ForceRepoll: true,
   263  	})
   264  	assertHiddenMerkleErrorType(t, err, libkb.HiddenMerkleErrorServerWitholdingLinks)
   265  }
   266  
   267  func loadTeamFTLAndAssertName(t *testing.T, tc *libkb.TestContext, teamID keybase1.TeamID, teamName keybase1.TeamName) {
   268  	res, err := tc.G.GetFastTeamLoader().Load(libkb.NewMetaContextForTest(*tc), keybase1.FastTeamLoadArg{
   269  		ID:           teamID,
   270  		ForceRefresh: true,
   271  	})
   272  	require.NoError(t, err)
   273  	require.Equal(t, res.Name.String(), teamName.String())
   274  }
   275  
   276  func loadTeamFTLAndAssertGeneration(t *testing.T, tc *libkb.TestContext, teamID keybase1.TeamID, teamName keybase1.TeamName, perTeamKeyGeneration int) {
   277  	res, err := tc.G.GetFastTeamLoader().Load(libkb.NewMetaContextForTest(*tc), keybase1.FastTeamLoadArg{
   278  		ID:                   teamID,
   279  		ForceRefresh:         true,
   280  		Applications:         []keybase1.TeamApplication{keybase1.TeamApplication_CHAT},
   281  		KeyGenerationsNeeded: []keybase1.PerTeamKeyGeneration{keybase1.PerTeamKeyGeneration(perTeamKeyGeneration)},
   282  	})
   283  	require.NoError(t, err)
   284  	require.Equal(t, res.Name.String(), teamName.String())
   285  	require.Equal(t, 1, len(res.ApplicationKeys))
   286  	require.EqualValues(t, perTeamKeyGeneration, res.ApplicationKeys[0].KeyGeneration)
   287  }
   288  
   289  func loadTeamFTLAndAssertGenerationUnavailable(t *testing.T, tc *libkb.TestContext, teamID keybase1.TeamID, perTeamKeyGeneration int) {
   290  	_, err := tc.G.GetFastTeamLoader().Load(libkb.NewMetaContextForTest(*tc), keybase1.FastTeamLoadArg{
   291  		ID:                   teamID,
   292  		ForceRefresh:         true,
   293  		Applications:         []keybase1.TeamApplication{keybase1.TeamApplication_CHAT},
   294  		KeyGenerationsNeeded: []keybase1.PerTeamKeyGeneration{keybase1.PerTeamKeyGeneration(perTeamKeyGeneration)},
   295  	})
   296  	require.Error(t, err)
   297  	require.IsType(t, FTLMissingSeedError{}, err)
   298  }
   299  
   300  func loadTeamFTLAndAssertMaxGeneration(t *testing.T, tc *libkb.TestContext, teamID keybase1.TeamID, teamName keybase1.TeamName, perTeamKeyGeneration int) {
   301  	_, err := tc.G.GetMerkleClient().FetchRootFromServer(libkb.NewMetaContextForTest(*tc), 0)
   302  	require.NoError(t, err)
   303  	loadTeamFTLAndAssertGeneration(t, tc, teamID, teamName, perTeamKeyGeneration)
   304  	loadTeamFTLAndAssertGenerationUnavailable(t, tc, teamID, perTeamKeyGeneration+1)
   305  }
   306  
   307  func TestFTLSucceedsIfServerDoesntCommitLinks(t *testing.T) {
   308  	retryTestNTimes(t, 5, testFTLSucceedsIfServerDoesntCommitLinks)
   309  }
   310  
   311  func testFTLSucceedsIfServerDoesntCommitLinks(t *testing.T) bool {
   312  	fus, tcs, cleanup := setupNTests(t, 2)
   313  	defer cleanup()
   314  
   315  	clock := clockwork.NewFakeClock()
   316  	tcs[1].G.SetClock(clock)
   317  
   318  	t.Logf("create team")
   319  	teamName, teamID := createTeam2(*tcs[0])
   320  
   321  	t.Logf("add B to the team so they can load it")
   322  	_, err := AddMember(context.TODO(), tcs[0].G, teamName.String(), fus[1].Username, keybase1.TeamRole_WRITER, nil)
   323  	require.NoError(t, err)
   324  
   325  	loadTeamFTLAndAssertName(t, tcs[1], teamID, teamName)
   326  
   327  	makeHiddenRotation(t, tcs[0].G, teamName)
   328  
   329  	loadTeamFTLAndAssertMaxGeneration(t, tcs[1], teamID, teamName, 2)
   330  
   331  	requestNewBlindTreeFromArchitectAndWaitUntilDone(t, tcs[0])
   332  
   333  	// make another hidden rotation
   334  	makeHiddenRotation(t, tcs[0].G, teamName)
   335  
   336  	loadTeamFTLAndAssertMaxGeneration(t, tcs[1], teamID, teamName, 3)
   337  
   338  	// now, move the clock forward and reload. The hidden loader should complain about hidden seqno 2 not being committed
   339  	clock.Advance(2 * hidden.MaxDelayInCommittingHiddenLinks)
   340  	tcs[1].G.SetClock(clock)
   341  	_, err = tcs[1].G.GetFastTeamLoader().Load(libkb.NewMetaContextForTest(*tcs[1]), keybase1.FastTeamLoadArg{
   342  		ID:                   teamID,
   343  		ForceRefresh:         true,
   344  		Applications:         []keybase1.TeamApplication{keybase1.TeamApplication_CHAT},
   345  		KeyGenerationsNeeded: []keybase1.PerTeamKeyGeneration{keybase1.PerTeamKeyGeneration(3)},
   346  	})
   347  
   348  	// This has the potential to flake, if the architect runs concurrently and does make a new blind tree version.
   349  	return err == nil
   350  }
   351  
   352  func TestFTLFailsIfServerRollsbackUncommittedSeqno(t *testing.T) {
   353  	fus, tcs, cleanup := setupNTests(t, 2)
   354  	defer cleanup()
   355  
   356  	t.Logf("create team")
   357  	teamName, teamID := createTeam2(*tcs[0])
   358  
   359  	t.Logf("add B to the team so they can load it")
   360  	_, err := AddMember(context.TODO(), tcs[0].G, teamName.String(), fus[1].Username, keybase1.TeamRole_WRITER, nil)
   361  	require.NoError(t, err)
   362  
   363  	loadTeamFTLAndAssertMaxGeneration(t, tcs[1], teamID, teamName, 1)
   364  
   365  	makeHiddenRotation(t, tcs[0].G, teamName)
   366  
   367  	loadTeamFTLAndAssertMaxGeneration(t, tcs[1], teamID, teamName, 2)
   368  
   369  	// now load the team again, but this time we change the response of the server to rollback the number of committed sequence numbers
   370  	newLoader := tcs[1].G.GetFastTeamLoader()
   371  	newLoader.(*FastTeamChainLoader).world = CorruptingMockLoaderContext{
   372  		LoaderContext: newLoader.(*FastTeamChainLoader).world,
   373  		merkleCorruptorFunc: func(r1 keybase1.Seqno, r2 keybase1.LinkID, hiddenResp *libkb.MerkleHiddenResponse, lastMerkleRoot *libkb.MerkleRoot, err error) (keybase1.Seqno, keybase1.LinkID, *libkb.MerkleHiddenResponse, *libkb.MerkleRoot, error) {
   374  			if hiddenResp != nil && hiddenResp.UncommittedSeqno >= 1 {
   375  				hiddenResp.UncommittedSeqno--
   376  				t.Logf("Simulating malicious server: updating hiddenResp.UncommittedSeqno (new value %v)", hiddenResp.UncommittedSeqno)
   377  			}
   378  			return r1, r2, hiddenResp, lastMerkleRoot, err
   379  		},
   380  	}
   381  	tcs[1].G.SetFastTeamLoader(newLoader)
   382  
   383  	_, err = tcs[1].G.GetFastTeamLoader().Load(libkb.NewMetaContextForTest(*tcs[1]), keybase1.FastTeamLoadArg{
   384  		ID:                   teamID,
   385  		ForceRefresh:         true,
   386  		Applications:         []keybase1.TeamApplication{keybase1.TeamApplication_CHAT},
   387  		KeyGenerationsNeeded: []keybase1.PerTeamKeyGeneration{keybase1.PerTeamKeyGeneration(2)},
   388  	})
   389  	assertHiddenMerkleErrorType(t, err, libkb.HiddenMerkleErrorRollbackUncommittedSeqno)
   390  }
   391  
   392  func TestFTLFailsIfServerDoesNotReturnPromisedLinks(t *testing.T) {
   393  	fus, tcs, cleanup := setupNTests(t, 2)
   394  	defer cleanup()
   395  
   396  	t.Logf("create team")
   397  	teamName, teamID := createTeam2(*tcs[0])
   398  
   399  	t.Logf("add B to the team so they can load it")
   400  	_, err := AddMember(context.TODO(), tcs[0].G, teamName.String(), fus[1].Username, keybase1.TeamRole_WRITER, nil)
   401  	require.NoError(t, err)
   402  
   403  	loadTeamFTLAndAssertMaxGeneration(t, tcs[1], teamID, teamName, 1)
   404  
   405  	makeHiddenRotation(t, tcs[0].G, teamName)
   406  
   407  	loadTeamFTLAndAssertMaxGeneration(t, tcs[1], teamID, teamName, 2)
   408  
   409  	makeHiddenRotation(t, tcs[0].G, teamName)
   410  
   411  	// now load the team again, but this time we change the response of the server as if there were more hidden links
   412  	newLoader := tcs[1].G.GetFastTeamLoader()
   413  	newLoader.(*FastTeamChainLoader).world = CorruptingMockLoaderContext{
   414  		LoaderContext: newLoader.(*FastTeamChainLoader).world,
   415  		merkleCorruptorFunc: func(r1 keybase1.Seqno, r2 keybase1.LinkID, hiddenResp *libkb.MerkleHiddenResponse, lastMerkleRoot *libkb.MerkleRoot, err error) (keybase1.Seqno, keybase1.LinkID, *libkb.MerkleHiddenResponse, *libkb.MerkleRoot, error) {
   416  			if hiddenResp != nil && hiddenResp.UncommittedSeqno >= 1 {
   417  				hiddenResp.UncommittedSeqno += 5
   418  				t.Logf("Simulating malicious server: updating hiddenResp.UncommittedSeqno (new value %v)", hiddenResp.UncommittedSeqno)
   419  			}
   420  			return r1, r2, hiddenResp, lastMerkleRoot, err
   421  		},
   422  	}
   423  	tcs[1].G.SetFastTeamLoader(newLoader)
   424  
   425  	_, err = tcs[1].G.GetFastTeamLoader().Load(libkb.NewMetaContextForTest(*tcs[1]), keybase1.FastTeamLoadArg{
   426  		ID:                   teamID,
   427  		ForceRefresh:         true,
   428  		Applications:         []keybase1.TeamApplication{keybase1.TeamApplication_CHAT},
   429  		KeyGenerationsNeeded: []keybase1.PerTeamKeyGeneration{keybase1.PerTeamKeyGeneration(3)},
   430  	})
   431  	assertHiddenMerkleErrorType(t, err, libkb.HiddenMerkleErrorServerWitholdingLinks)
   432  }
   433  
   434  func TestSubteamReaderFTL(t *testing.T) {
   435  	fus, tcs, cleanup := setupNTests(t, 3)
   436  	defer cleanup()
   437  
   438  	t.Logf("create team")
   439  	teamName, teamID := createTeam2(*tcs[0])
   440  	for i := 0; i < 4; i++ {
   441  		makeHiddenRotation(t, tcs[0].G, teamName)
   442  	}
   443  	createSubteam(tcs[0], teamName, "unused")
   444  	subteamName, subteamID := createSubteam(tcs[0], teamName, "sub")
   445  	_, err := AddMember(context.TODO(), tcs[0].G, subteamName.String(), fus[1].Username, keybase1.TeamRole_WRITER, nil)
   446  	require.NoError(t, err)
   447  	mctx := libkb.NewMetaContextForTest(*tcs[1])
   448  	_, err = mctx.G().GetFastTeamLoader().Load(mctx, keybase1.FastTeamLoadArg{
   449  		ID:                   subteamID,
   450  		ForceRefresh:         true,
   451  		Applications:         []keybase1.TeamApplication{keybase1.TeamApplication_CHAT},
   452  		KeyGenerationsNeeded: []keybase1.PerTeamKeyGeneration{keybase1.PerTeamKeyGeneration(1)},
   453  	})
   454  	require.NoError(t, err)
   455  
   456  	// Check that U1's hidden team data for the parent team is still stored, and that
   457  	// the ratchets persist (even though there's no other data there).
   458  	var htc *keybase1.HiddenTeamChain
   459  	htc, err = mctx.G().GetHiddenTeamChainManager().Load(mctx, teamID)
   460  	require.NoError(t, err)
   461  	require.NotNil(t, htc)
   462  	ratchet, ok := htc.RatchetSet.Ratchets[keybase1.RatchetType_MAIN]
   463  	require.True(t, ok)
   464  	require.Equal(t, ratchet.Triple.Seqno, keybase1.Seqno(4))
   465  	require.Equal(t, htc.Last, keybase1.Seqno(0))
   466  	require.Equal(t, len(htc.Outer), 0)
   467  }