You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

522 lines
18 KiB

  1. package light_test
  2. import (
  3. "bytes"
  4. "testing"
  5. "time"
  6. "github.com/stretchr/testify/assert"
  7. "github.com/stretchr/testify/mock"
  8. "github.com/stretchr/testify/require"
  9. dbm "github.com/tendermint/tm-db"
  10. "github.com/tendermint/tendermint/libs/log"
  11. "github.com/tendermint/tendermint/light"
  12. "github.com/tendermint/tendermint/light/provider"
  13. provider_mocks "github.com/tendermint/tendermint/light/provider/mocks"
  14. dbs "github.com/tendermint/tendermint/light/store/db"
  15. "github.com/tendermint/tendermint/types"
  16. )
  17. func TestLightClientAttackEvidence_Lunatic(t *testing.T) {
  18. logger := log.NewTestingLogger(t)
  19. // primary performs a lunatic attack
  20. var (
  21. latestHeight = int64(3)
  22. valSize = 5
  23. divergenceHeight = int64(2)
  24. primaryHeaders = make(map[int64]*types.SignedHeader, latestHeight)
  25. primaryValidators = make(map[int64]*types.ValidatorSet, latestHeight)
  26. )
  27. witnessHeaders, witnessValidators, chainKeys := genLightBlocksWithKeys(chainID, latestHeight, valSize, 2, bTime)
  28. forgedKeys := chainKeys[divergenceHeight-1].ChangeKeys(3) // we change 3 out of the 5 validators (still 2/5 remain)
  29. forgedVals := forgedKeys.ToValidators(2, 0)
  30. for height := int64(1); height <= latestHeight; height++ {
  31. if height < divergenceHeight {
  32. primaryHeaders[height] = witnessHeaders[height]
  33. primaryValidators[height] = witnessValidators[height]
  34. continue
  35. }
  36. primaryHeaders[height] = forgedKeys.GenSignedHeader(chainID, height, bTime.Add(time.Duration(height)*time.Minute),
  37. nil, forgedVals, forgedVals, hash("app_hash"), hash("cons_hash"), hash("results_hash"), 0, len(forgedKeys))
  38. primaryValidators[height] = forgedVals
  39. }
  40. // never called, delete it to make mockery asserts pass
  41. delete(witnessHeaders, 2)
  42. delete(primaryHeaders, 2)
  43. mockWitness := mockNodeFromHeadersAndVals(witnessHeaders, witnessValidators)
  44. mockPrimary := mockNodeFromHeadersAndVals(primaryHeaders, primaryValidators)
  45. mockWitness.On("ReportEvidence", mock.Anything, mock.MatchedBy(func(evidence types.Evidence) bool {
  46. evAgainstPrimary := &types.LightClientAttackEvidence{
  47. // after the divergence height the valset doesn't change so we expect the evidence to be for the latest height
  48. ConflictingBlock: &types.LightBlock{
  49. SignedHeader: primaryHeaders[latestHeight],
  50. ValidatorSet: primaryValidators[latestHeight],
  51. },
  52. CommonHeight: 1,
  53. }
  54. return bytes.Equal(evidence.Hash(), evAgainstPrimary.Hash())
  55. })).Return(nil)
  56. mockPrimary.On("ReportEvidence", mock.Anything, mock.MatchedBy(func(evidence types.Evidence) bool {
  57. evAgainstWitness := &types.LightClientAttackEvidence{
  58. // when forming evidence against witness we learn that the canonical chain continued to change validator sets
  59. // hence the conflicting block is at 7
  60. ConflictingBlock: &types.LightBlock{
  61. SignedHeader: witnessHeaders[divergenceHeight+1],
  62. ValidatorSet: witnessValidators[divergenceHeight+1],
  63. },
  64. CommonHeight: divergenceHeight - 1,
  65. }
  66. return bytes.Equal(evidence.Hash(), evAgainstWitness.Hash())
  67. })).Return(nil)
  68. c, err := light.NewClient(
  69. ctx,
  70. chainID,
  71. light.TrustOptions{
  72. Period: 4 * time.Hour,
  73. Height: 1,
  74. Hash: primaryHeaders[1].Hash(),
  75. },
  76. mockPrimary,
  77. []provider.Provider{mockWitness},
  78. dbs.New(dbm.NewMemDB()),
  79. light.Logger(logger),
  80. )
  81. require.NoError(t, err)
  82. // Check verification returns an error.
  83. _, err = c.VerifyLightBlockAtHeight(ctx, latestHeight, bTime.Add(1*time.Hour))
  84. if assert.Error(t, err) {
  85. assert.Equal(t, light.ErrLightClientAttack, err)
  86. }
  87. mockWitness.AssertExpectations(t)
  88. mockPrimary.AssertExpectations(t)
  89. }
  90. func TestLightClientAttackEvidence_Equivocation(t *testing.T) {
  91. cases := []struct {
  92. name string
  93. lightOption light.Option
  94. unusedWitnessBlockHeights []int64
  95. unusedPrimaryBlockHeights []int64
  96. latestHeight int64
  97. divergenceHeight int64
  98. }{
  99. {
  100. name: "sequential",
  101. lightOption: light.SequentialVerification(),
  102. unusedWitnessBlockHeights: []int64{4, 6},
  103. latestHeight: int64(5),
  104. divergenceHeight: int64(3),
  105. },
  106. {
  107. name: "skipping",
  108. lightOption: light.SkippingVerification(light.DefaultTrustLevel),
  109. unusedWitnessBlockHeights: []int64{2, 4, 6},
  110. unusedPrimaryBlockHeights: []int64{2, 4, 6},
  111. latestHeight: int64(5),
  112. divergenceHeight: int64(3),
  113. },
  114. }
  115. for _, tc := range cases {
  116. testCase := tc
  117. t.Run(testCase.name, func(t *testing.T) {
  118. logger := log.NewTestingLogger(t)
  119. // primary performs an equivocation attack
  120. var (
  121. valSize = 5
  122. primaryHeaders = make(map[int64]*types.SignedHeader, testCase.latestHeight)
  123. // validators don't change in this network (however we still use a map just for convenience)
  124. primaryValidators = make(map[int64]*types.ValidatorSet, testCase.latestHeight)
  125. )
  126. witnessHeaders, witnessValidators, chainKeys := genLightBlocksWithKeys(chainID,
  127. testCase.latestHeight+1, valSize, 2, bTime)
  128. for height := int64(1); height <= testCase.latestHeight; height++ {
  129. if height < testCase.divergenceHeight {
  130. primaryHeaders[height] = witnessHeaders[height]
  131. primaryValidators[height] = witnessValidators[height]
  132. continue
  133. }
  134. // we don't have a network partition so we will make 4/5 (greater than 2/3) malicious and vote again for
  135. // a different block (which we do by adding txs)
  136. primaryHeaders[height] = chainKeys[height].GenSignedHeader(chainID, height,
  137. bTime.Add(time.Duration(height)*time.Minute), []types.Tx{[]byte("abcd")},
  138. witnessValidators[height], witnessValidators[height+1], hash("app_hash"),
  139. hash("cons_hash"), hash("results_hash"), 0, len(chainKeys[height])-1)
  140. primaryValidators[height] = witnessValidators[height]
  141. }
  142. for _, height := range testCase.unusedWitnessBlockHeights {
  143. delete(witnessHeaders, height)
  144. }
  145. mockWitness := mockNodeFromHeadersAndVals(witnessHeaders, witnessValidators)
  146. for _, height := range testCase.unusedPrimaryBlockHeights {
  147. delete(primaryHeaders, height)
  148. }
  149. mockPrimary := mockNodeFromHeadersAndVals(primaryHeaders, primaryValidators)
  150. // Check evidence was sent to both full nodes.
  151. // Common height should be set to the height of the divergent header in the instance
  152. // of an equivocation attack and the validator sets are the same as what the witness has
  153. mockWitness.On("ReportEvidence", mock.Anything, mock.MatchedBy(func(evidence types.Evidence) bool {
  154. evAgainstPrimary := &types.LightClientAttackEvidence{
  155. ConflictingBlock: &types.LightBlock{
  156. SignedHeader: primaryHeaders[testCase.divergenceHeight],
  157. ValidatorSet: primaryValidators[testCase.divergenceHeight],
  158. },
  159. CommonHeight: testCase.divergenceHeight,
  160. }
  161. return bytes.Equal(evidence.Hash(), evAgainstPrimary.Hash())
  162. })).Return(nil)
  163. mockPrimary.On("ReportEvidence", mock.Anything, mock.MatchedBy(func(evidence types.Evidence) bool {
  164. evAgainstWitness := &types.LightClientAttackEvidence{
  165. ConflictingBlock: &types.LightBlock{
  166. SignedHeader: witnessHeaders[testCase.divergenceHeight],
  167. ValidatorSet: witnessValidators[testCase.divergenceHeight],
  168. },
  169. CommonHeight: testCase.divergenceHeight,
  170. }
  171. return bytes.Equal(evidence.Hash(), evAgainstWitness.Hash())
  172. })).Return(nil)
  173. c, err := light.NewClient(
  174. ctx,
  175. chainID,
  176. light.TrustOptions{
  177. Period: 4 * time.Hour,
  178. Height: 1,
  179. Hash: primaryHeaders[1].Hash(),
  180. },
  181. mockPrimary,
  182. []provider.Provider{mockWitness},
  183. dbs.New(dbm.NewMemDB()),
  184. light.Logger(logger),
  185. testCase.lightOption,
  186. )
  187. require.NoError(t, err)
  188. // Check verification returns an error.
  189. _, err = c.VerifyLightBlockAtHeight(ctx, testCase.latestHeight, bTime.Add(300*time.Second))
  190. if assert.Error(t, err) {
  191. assert.Equal(t, light.ErrLightClientAttack, err)
  192. }
  193. mockWitness.AssertExpectations(t)
  194. mockPrimary.AssertExpectations(t)
  195. })
  196. }
  197. }
  198. func TestLightClientAttackEvidence_ForwardLunatic(t *testing.T) {
  199. // primary performs a lunatic attack but changes the time of the header to
  200. // something in the future relative to the blockchain
  201. var (
  202. latestHeight = int64(10)
  203. valSize = 5
  204. forgedHeight = int64(12)
  205. proofHeight = int64(11)
  206. primaryHeaders = make(map[int64]*types.SignedHeader, forgedHeight)
  207. primaryValidators = make(map[int64]*types.ValidatorSet, forgedHeight)
  208. )
  209. logger := log.NewTestingLogger(t)
  210. witnessHeaders, witnessValidators, chainKeys := genLightBlocksWithKeys(chainID, latestHeight, valSize, 2, bTime)
  211. for _, unusedHeader := range []int64{3, 5, 6, 8} {
  212. delete(witnessHeaders, unusedHeader)
  213. }
  214. // primary has the exact same headers except it forges one extra header in the future using keys from 2/5ths of
  215. // the validators
  216. for h := range witnessHeaders {
  217. primaryHeaders[h] = witnessHeaders[h]
  218. primaryValidators[h] = witnessValidators[h]
  219. }
  220. for _, unusedHeader := range []int64{3, 5, 6, 8} {
  221. delete(primaryHeaders, unusedHeader)
  222. }
  223. forgedKeys := chainKeys[latestHeight].ChangeKeys(3) // we change 3 out of the 5 validators (still 2/5 remain)
  224. primaryValidators[forgedHeight] = forgedKeys.ToValidators(2, 0)
  225. primaryHeaders[forgedHeight] = forgedKeys.GenSignedHeader(
  226. chainID,
  227. forgedHeight,
  228. bTime.Add(time.Duration(latestHeight+1)*time.Minute), // 11 mins
  229. nil,
  230. primaryValidators[forgedHeight],
  231. primaryValidators[forgedHeight],
  232. hash("app_hash"),
  233. hash("cons_hash"),
  234. hash("results_hash"),
  235. 0, len(forgedKeys),
  236. )
  237. mockPrimary := mockNodeFromHeadersAndVals(primaryHeaders, primaryValidators)
  238. lastBlock, _ := mockPrimary.LightBlock(ctx, forgedHeight)
  239. mockPrimary.On("LightBlock", mock.Anything, int64(0)).Return(lastBlock, nil)
  240. mockPrimary.On("LightBlock", mock.Anything, mock.Anything).Return(nil, provider.ErrLightBlockNotFound)
  241. /*
  242. for _, unusedHeader := range []int64{3, 5, 6, 8} {
  243. delete(witnessHeaders, unusedHeader)
  244. }
  245. */
  246. mockWitness := mockNodeFromHeadersAndVals(witnessHeaders, witnessValidators)
  247. lastBlock, _ = mockWitness.LightBlock(ctx, latestHeight)
  248. mockWitness.On("LightBlock", mock.Anything, int64(0)).Return(lastBlock, nil).Once()
  249. mockWitness.On("LightBlock", mock.Anything, int64(12)).Return(nil, provider.ErrHeightTooHigh)
  250. mockWitness.On("ReportEvidence", mock.Anything, mock.MatchedBy(func(evidence types.Evidence) bool {
  251. // Check evidence was sent to the witness against the full node
  252. evAgainstPrimary := &types.LightClientAttackEvidence{
  253. ConflictingBlock: &types.LightBlock{
  254. SignedHeader: primaryHeaders[forgedHeight],
  255. ValidatorSet: primaryValidators[forgedHeight],
  256. },
  257. CommonHeight: latestHeight,
  258. }
  259. return bytes.Equal(evidence.Hash(), evAgainstPrimary.Hash())
  260. })).Return(nil).Twice()
  261. // In order to perform the attack, the primary needs at least one accomplice as a witness to also
  262. // send the forged block
  263. accomplice := mockPrimary
  264. c, err := light.NewClient(
  265. ctx,
  266. chainID,
  267. light.TrustOptions{
  268. Period: 4 * time.Hour,
  269. Height: 1,
  270. Hash: primaryHeaders[1].Hash(),
  271. },
  272. mockPrimary,
  273. []provider.Provider{mockWitness, accomplice},
  274. dbs.New(dbm.NewMemDB()),
  275. light.Logger(logger),
  276. light.MaxClockDrift(1*time.Second),
  277. light.MaxBlockLag(1*time.Second),
  278. )
  279. require.NoError(t, err)
  280. // two seconds later, the supporting withness should receive the header that can be used
  281. // to prove that there was an attack
  282. vals := chainKeys[latestHeight].ToValidators(2, 0)
  283. newLb := &types.LightBlock{
  284. SignedHeader: chainKeys[latestHeight].GenSignedHeader(
  285. chainID,
  286. proofHeight,
  287. bTime.Add(time.Duration(proofHeight+1)*time.Minute), // 12 mins
  288. nil,
  289. vals,
  290. vals,
  291. hash("app_hash"),
  292. hash("cons_hash"),
  293. hash("results_hash"),
  294. 0, len(chainKeys),
  295. ),
  296. ValidatorSet: vals,
  297. }
  298. go func() {
  299. time.Sleep(2 * time.Second)
  300. mockWitness.On("LightBlock", mock.Anything, int64(0)).Return(newLb, nil)
  301. }()
  302. // Now assert that verification returns an error. We craft the light clients time to be a little ahead of the chain
  303. // to allow a window for the attack to manifest itself.
  304. _, err = c.Update(ctx, bTime.Add(time.Duration(forgedHeight)*time.Minute))
  305. if assert.Error(t, err) {
  306. assert.Equal(t, light.ErrLightClientAttack, err)
  307. }
  308. // We attempt the same call but now the supporting witness has a block which should
  309. // immediately conflict in time with the primary
  310. _, err = c.VerifyLightBlockAtHeight(ctx, forgedHeight, bTime.Add(time.Duration(forgedHeight)*time.Minute))
  311. if assert.Error(t, err) {
  312. assert.Equal(t, light.ErrLightClientAttack, err)
  313. }
  314. // Lastly we test the unfortunate case where the light clients supporting witness doesn't update
  315. // in enough time
  316. mockLaggingWitness := mockNodeFromHeadersAndVals(witnessHeaders, witnessValidators)
  317. mockLaggingWitness.On("LightBlock", mock.Anything, int64(12)).Return(nil, provider.ErrHeightTooHigh)
  318. lastBlock, _ = mockLaggingWitness.LightBlock(ctx, latestHeight)
  319. mockLaggingWitness.On("LightBlock", mock.Anything, int64(0)).Return(lastBlock, nil)
  320. c, err = light.NewClient(
  321. ctx,
  322. chainID,
  323. light.TrustOptions{
  324. Period: 4 * time.Hour,
  325. Height: 1,
  326. Hash: primaryHeaders[1].Hash(),
  327. },
  328. mockPrimary,
  329. []provider.Provider{mockLaggingWitness, accomplice},
  330. dbs.New(dbm.NewMemDB()),
  331. light.Logger(logger),
  332. light.MaxClockDrift(1*time.Second),
  333. light.MaxBlockLag(1*time.Second),
  334. )
  335. require.NoError(t, err)
  336. _, err = c.Update(ctx, bTime.Add(time.Duration(forgedHeight)*time.Minute))
  337. assert.NoError(t, err)
  338. mockPrimary.AssertExpectations(t)
  339. mockWitness.AssertExpectations(t)
  340. }
  341. // 1. Different nodes therefore a divergent header is produced.
  342. // => light client returns an error upon creation because primary and witness
  343. // have a different view.
  344. func TestClientDivergentTraces1(t *testing.T) {
  345. headers, vals, _ := genLightBlocksWithKeys(chainID, 1, 5, 2, bTime)
  346. mockPrimary := mockNodeFromHeadersAndVals(headers, vals)
  347. firstBlock, err := mockPrimary.LightBlock(ctx, 1)
  348. require.NoError(t, err)
  349. headers, vals, _ = genLightBlocksWithKeys(chainID, 1, 5, 2, bTime)
  350. mockWitness := mockNodeFromHeadersAndVals(headers, vals)
  351. logger := log.NewTestingLogger(t)
  352. _, err = light.NewClient(
  353. ctx,
  354. chainID,
  355. light.TrustOptions{
  356. Height: 1,
  357. Hash: firstBlock.Hash(),
  358. Period: 4 * time.Hour,
  359. },
  360. mockPrimary,
  361. []provider.Provider{mockWitness},
  362. dbs.New(dbm.NewMemDB()),
  363. light.Logger(logger),
  364. )
  365. require.Error(t, err)
  366. assert.Contains(t, err.Error(), "does not match primary")
  367. mockWitness.AssertExpectations(t)
  368. mockPrimary.AssertExpectations(t)
  369. }
  370. // 2. Two out of three nodes don't respond but the third has a header that matches
  371. // => verification should be successful and all the witnesses should remain
  372. func TestClientDivergentTraces2(t *testing.T) {
  373. logger := log.NewTestingLogger(t)
  374. headers, vals, _ := genLightBlocksWithKeys(chainID, 2, 5, 2, bTime)
  375. mockPrimaryNode := mockNodeFromHeadersAndVals(headers, vals)
  376. mockDeadNode := &provider_mocks.Provider{}
  377. mockDeadNode.On("LightBlock", mock.Anything, mock.Anything).Return(nil, provider.ErrNoResponse)
  378. firstBlock, err := mockPrimaryNode.LightBlock(ctx, 1)
  379. require.NoError(t, err)
  380. c, err := light.NewClient(
  381. ctx,
  382. chainID,
  383. light.TrustOptions{
  384. Height: 1,
  385. Hash: firstBlock.Hash(),
  386. Period: 4 * time.Hour,
  387. },
  388. mockPrimaryNode,
  389. []provider.Provider{mockDeadNode, mockDeadNode, mockPrimaryNode},
  390. dbs.New(dbm.NewMemDB()),
  391. light.Logger(logger),
  392. )
  393. require.NoError(t, err)
  394. _, err = c.VerifyLightBlockAtHeight(ctx, 2, bTime.Add(1*time.Hour))
  395. assert.NoError(t, err)
  396. assert.Equal(t, 3, len(c.Witnesses()))
  397. mockDeadNode.AssertExpectations(t)
  398. mockPrimaryNode.AssertExpectations(t)
  399. }
  400. // 3. witness has the same first header, but different second header
  401. // => creation should succeed, but the verification should fail
  402. //nolint: dupl
  403. func TestClientDivergentTraces3(t *testing.T) {
  404. logger := log.NewTestingLogger(t)
  405. //
  406. primaryHeaders, primaryVals, _ := genLightBlocksWithKeys(chainID, 2, 5, 2, bTime)
  407. mockPrimary := mockNodeFromHeadersAndVals(primaryHeaders, primaryVals)
  408. firstBlock, err := mockPrimary.LightBlock(ctx, 1)
  409. require.NoError(t, err)
  410. mockHeaders, mockVals, _ := genLightBlocksWithKeys(chainID, 2, 5, 2, bTime)
  411. mockHeaders[1] = primaryHeaders[1]
  412. mockVals[1] = primaryVals[1]
  413. mockWitness := mockNodeFromHeadersAndVals(mockHeaders, mockVals)
  414. c, err := light.NewClient(
  415. ctx,
  416. chainID,
  417. light.TrustOptions{
  418. Height: 1,
  419. Hash: firstBlock.Hash(),
  420. Period: 4 * time.Hour,
  421. },
  422. mockPrimary,
  423. []provider.Provider{mockWitness},
  424. dbs.New(dbm.NewMemDB()),
  425. light.Logger(logger),
  426. )
  427. require.NoError(t, err)
  428. _, err = c.VerifyLightBlockAtHeight(ctx, 2, bTime.Add(1*time.Hour))
  429. assert.Error(t, err)
  430. assert.Equal(t, 1, len(c.Witnesses()))
  431. mockWitness.AssertExpectations(t)
  432. mockPrimary.AssertExpectations(t)
  433. }
  434. // 4. Witness has a divergent header but can not produce a valid trace to back it up.
  435. // It should be ignored
  436. //nolint: dupl
  437. func TestClientDivergentTraces4(t *testing.T) {
  438. logger := log.NewTestingLogger(t)
  439. //
  440. primaryHeaders, primaryVals, _ := genLightBlocksWithKeys(chainID, 2, 5, 2, bTime)
  441. mockPrimary := mockNodeFromHeadersAndVals(primaryHeaders, primaryVals)
  442. firstBlock, err := mockPrimary.LightBlock(ctx, 1)
  443. require.NoError(t, err)
  444. witnessHeaders, witnessVals, _ := genLightBlocksWithKeys(chainID, 2, 5, 2, bTime)
  445. primaryHeaders[2] = witnessHeaders[2]
  446. primaryVals[2] = witnessVals[2]
  447. mockWitness := mockNodeFromHeadersAndVals(primaryHeaders, primaryVals)
  448. c, err := light.NewClient(
  449. ctx,
  450. chainID,
  451. light.TrustOptions{
  452. Height: 1,
  453. Hash: firstBlock.Hash(),
  454. Period: 4 * time.Hour,
  455. },
  456. mockPrimary,
  457. []provider.Provider{mockWitness},
  458. dbs.New(dbm.NewMemDB()),
  459. light.Logger(logger),
  460. )
  461. require.NoError(t, err)
  462. _, err = c.VerifyLightBlockAtHeight(ctx, 2, bTime.Add(1*time.Hour))
  463. assert.Error(t, err)
  464. assert.Equal(t, 1, len(c.Witnesses()))
  465. mockWitness.AssertExpectations(t)
  466. mockPrimary.AssertExpectations(t)
  467. }