Checkpoints.test.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. const { expectRevert, time } = require('@openzeppelin/test-helpers');
  2. const { expect } = require('chai');
  3. const { batchInBlock } = require('../helpers/txpool');
  4. const $Checkpoints = artifacts.require('$Checkpoints');
  5. // The library name may be 'Checkpoints' or 'CheckpointsUpgradeable'
  6. const libraryName = $Checkpoints._json.contractName.replace(/^\$/, '');
  7. const traceLengths = [160, 224];
  8. const first = array => (array.length ? array[0] : undefined);
  9. const last = array => (array.length ? array[array.length - 1] : undefined);
  10. contract('Checkpoints', function () {
  11. beforeEach(async function () {
  12. this.mock = await $Checkpoints.new();
  13. this.methods = { trace: {} };
  14. this.methods.history = {
  15. latest: (...args) => this.mock.methods[`$latest_${libraryName}_History(uint256)`](0, ...args),
  16. latestCheckpoint: (...args) => this.mock.methods[`$latestCheckpoint_${libraryName}_History(uint256)`](0, ...args),
  17. length: (...args) => this.mock.methods[`$length_${libraryName}_History(uint256)`](0, ...args),
  18. push: (...args) => this.mock.methods['$push(uint256,uint256)'](0, ...args),
  19. getAtBlock: (...args) => this.mock.$getAtBlock(0, ...args),
  20. getAtRecentBlock: (...args) => this.mock.$getAtProbablyRecentBlock(0, ...args),
  21. };
  22. for (const length of traceLengths) {
  23. this.methods.trace[length] = {
  24. latest: (...args) => this.mock.methods[`$latest_${libraryName}_Trace${length}(uint256)`](0, ...args),
  25. latestCheckpoint: (...args) =>
  26. this.mock.methods[`$latestCheckpoint_${libraryName}_Trace${length}(uint256)`](0, ...args),
  27. length: (...args) => this.mock.methods[`$length_${libraryName}_Trace${length}(uint256)`](0, ...args),
  28. push: (...args) => this.mock.methods[`$push(uint256,uint${256 - length},uint${length})`](0, ...args),
  29. lowerLookup: (...args) => this.mock.methods[`$lowerLookup(uint256,uint${256 - length})`](0, ...args),
  30. upperLookup: (...args) => this.mock.methods[`$upperLookup(uint256,uint${256 - length})`](0, ...args),
  31. upperLookupRecent: (...args) =>
  32. this.mock.methods[`$upperLookupRecent(uint256,uint${256 - length})`](0, ...args),
  33. };
  34. }
  35. });
  36. describe('History checkpoints', function () {
  37. describe('without checkpoints', function () {
  38. it('returns zero as latest value', async function () {
  39. expect(await this.methods.history.latest()).to.be.bignumber.equal('0');
  40. const ckpt = await this.methods.history.latestCheckpoint();
  41. expect(ckpt[0]).to.be.equal(false);
  42. expect(ckpt[1]).to.be.bignumber.equal('0');
  43. expect(ckpt[2]).to.be.bignumber.equal('0');
  44. });
  45. it('returns zero as past value', async function () {
  46. await time.advanceBlock();
  47. expect(await this.methods.history.getAtBlock((await web3.eth.getBlockNumber()) - 1)).to.be.bignumber.equal('0');
  48. expect(
  49. await this.methods.history.getAtRecentBlock((await web3.eth.getBlockNumber()) - 1),
  50. ).to.be.bignumber.equal('0');
  51. });
  52. });
  53. describe('with checkpoints', function () {
  54. beforeEach('pushing checkpoints', async function () {
  55. this.tx1 = await this.methods.history.push(1);
  56. this.tx2 = await this.methods.history.push(2);
  57. await time.advanceBlock();
  58. this.tx3 = await this.methods.history.push(3);
  59. await time.advanceBlock();
  60. await time.advanceBlock();
  61. });
  62. it('returns latest value', async function () {
  63. expect(await this.methods.history.latest()).to.be.bignumber.equal('3');
  64. const ckpt = await this.methods.history.latestCheckpoint();
  65. expect(ckpt[0]).to.be.equal(true);
  66. expect(ckpt[1]).to.be.bignumber.equal(web3.utils.toBN(this.tx3.receipt.blockNumber));
  67. expect(ckpt[2]).to.be.bignumber.equal(web3.utils.toBN('3'));
  68. });
  69. for (const getAtBlockVariant of ['getAtBlock', 'getAtRecentBlock']) {
  70. describe(`lookup: ${getAtBlockVariant}`, function () {
  71. it('returns past values', async function () {
  72. expect(
  73. await this.methods.history[getAtBlockVariant](this.tx1.receipt.blockNumber - 1),
  74. ).to.be.bignumber.equal('0');
  75. expect(await this.methods.history[getAtBlockVariant](this.tx1.receipt.blockNumber)).to.be.bignumber.equal(
  76. '1',
  77. );
  78. expect(await this.methods.history[getAtBlockVariant](this.tx2.receipt.blockNumber)).to.be.bignumber.equal(
  79. '2',
  80. );
  81. // Block with no new checkpoints
  82. expect(
  83. await this.methods.history[getAtBlockVariant](this.tx2.receipt.blockNumber + 1),
  84. ).to.be.bignumber.equal('2');
  85. expect(await this.methods.history[getAtBlockVariant](this.tx3.receipt.blockNumber)).to.be.bignumber.equal(
  86. '3',
  87. );
  88. expect(
  89. await this.methods.history[getAtBlockVariant](this.tx3.receipt.blockNumber + 1),
  90. ).to.be.bignumber.equal('3');
  91. });
  92. it('reverts if block number >= current block', async function () {
  93. await expectRevert(
  94. this.methods.history[getAtBlockVariant](await web3.eth.getBlockNumber()),
  95. 'Checkpoints: block not yet mined',
  96. );
  97. await expectRevert(
  98. this.methods.history[getAtBlockVariant]((await web3.eth.getBlockNumber()) + 1),
  99. 'Checkpoints: block not yet mined',
  100. );
  101. });
  102. });
  103. }
  104. it('multiple checkpoints in the same block', async function () {
  105. const lengthBefore = await this.methods.history.length();
  106. await batchInBlock([
  107. () => this.methods.history.push(8, { gas: 100000 }),
  108. () => this.methods.history.push(9, { gas: 100000 }),
  109. () => this.methods.history.push(10, { gas: 100000 }),
  110. ]);
  111. expect(await this.methods.history.length()).to.be.bignumber.equal(lengthBefore.addn(1));
  112. expect(await this.methods.history.latest()).to.be.bignumber.equal('10');
  113. });
  114. it('more than 5 checkpoints', async function () {
  115. for (let i = 4; i <= 6; i++) {
  116. await this.methods.history.push(i);
  117. }
  118. expect(await this.methods.history.length()).to.be.bignumber.equal('6');
  119. const block = await web3.eth.getBlockNumber();
  120. // recent
  121. expect(await this.methods.history.getAtRecentBlock(block - 1)).to.be.bignumber.equal('5');
  122. // non-recent
  123. expect(await this.methods.history.getAtRecentBlock(block - 9)).to.be.bignumber.equal('0');
  124. });
  125. });
  126. });
  127. for (const length of traceLengths) {
  128. describe(`Trace${length}`, function () {
  129. describe('without checkpoints', function () {
  130. it('returns zero as latest value', async function () {
  131. expect(await this.methods.trace[length].latest()).to.be.bignumber.equal('0');
  132. const ckpt = await this.methods.trace[length].latestCheckpoint();
  133. expect(ckpt[0]).to.be.equal(false);
  134. expect(ckpt[1]).to.be.bignumber.equal('0');
  135. expect(ckpt[2]).to.be.bignumber.equal('0');
  136. });
  137. it('lookup returns 0', async function () {
  138. expect(await this.methods.trace[length].lowerLookup(0)).to.be.bignumber.equal('0');
  139. expect(await this.methods.trace[length].upperLookup(0)).to.be.bignumber.equal('0');
  140. expect(await this.methods.trace[length].upperLookupRecent(0)).to.be.bignumber.equal('0');
  141. });
  142. });
  143. describe('with checkpoints', function () {
  144. beforeEach('pushing checkpoints', async function () {
  145. this.checkpoints = [
  146. { key: '2', value: '17' },
  147. { key: '3', value: '42' },
  148. { key: '5', value: '101' },
  149. { key: '7', value: '23' },
  150. { key: '11', value: '99' },
  151. ];
  152. for (const { key, value } of this.checkpoints) {
  153. await this.methods.trace[length].push(key, value);
  154. }
  155. });
  156. it('length', async function () {
  157. expect(await this.methods.trace[length].length()).to.be.bignumber.equal(this.checkpoints.length.toString());
  158. });
  159. it('returns latest value', async function () {
  160. expect(await this.methods.trace[length].latest()).to.be.bignumber.equal(last(this.checkpoints).value);
  161. const ckpt = await this.methods.trace[length].latestCheckpoint();
  162. expect(ckpt[0]).to.be.equal(true);
  163. expect(ckpt[1]).to.be.bignumber.equal(last(this.checkpoints).key);
  164. expect(ckpt[2]).to.be.bignumber.equal(last(this.checkpoints).value);
  165. });
  166. it('cannot push values in the past', async function () {
  167. await expectRevert(
  168. this.methods.trace[length].push(last(this.checkpoints).key - 1, '0'),
  169. 'Checkpoint: decreasing keys',
  170. );
  171. });
  172. it('can update last value', async function () {
  173. const newValue = '42';
  174. // check length before the update
  175. expect(await this.methods.trace[length].length()).to.be.bignumber.equal(this.checkpoints.length.toString());
  176. // update last key
  177. await this.methods.trace[length].push(last(this.checkpoints).key, newValue);
  178. expect(await this.methods.trace[length].latest()).to.be.bignumber.equal(newValue);
  179. // check that length did not change
  180. expect(await this.methods.trace[length].length()).to.be.bignumber.equal(this.checkpoints.length.toString());
  181. });
  182. it('lower lookup', async function () {
  183. for (let i = 0; i < 14; ++i) {
  184. const value = first(this.checkpoints.filter(x => i <= x.key))?.value || '0';
  185. expect(await this.methods.trace[length].lowerLookup(i)).to.be.bignumber.equal(value);
  186. }
  187. });
  188. it('upper lookup & upperLookupRecent', async function () {
  189. for (let i = 0; i < 14; ++i) {
  190. const value = last(this.checkpoints.filter(x => i >= x.key))?.value || '0';
  191. expect(await this.methods.trace[length].upperLookup(i)).to.be.bignumber.equal(value);
  192. expect(await this.methods.trace[length].upperLookupRecent(i)).to.be.bignumber.equal(value);
  193. }
  194. });
  195. it('upperLookupRecent with more than 5 checkpoints', async function () {
  196. const moreCheckpoints = [
  197. { key: '12', value: '22' },
  198. { key: '13', value: '131' },
  199. { key: '17', value: '45' },
  200. { key: '19', value: '31452' },
  201. { key: '21', value: '0' },
  202. ];
  203. const allCheckpoints = [].concat(this.checkpoints, moreCheckpoints);
  204. for (const { key, value } of moreCheckpoints) {
  205. await this.methods.trace[length].push(key, value);
  206. }
  207. for (let i = 0; i < 25; ++i) {
  208. const value = last(allCheckpoints.filter(x => i >= x.key))?.value || '0';
  209. expect(await this.methods.trace[length].upperLookup(i)).to.be.bignumber.equal(value);
  210. expect(await this.methods.trace[length].upperLookupRecent(i)).to.be.bignumber.equal(value);
  211. }
  212. });
  213. });
  214. });
  215. }
  216. });