Checkpoints.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. const format = require('../format-lines');
  2. // OPTIONS
  3. const defaultOpts = size => ({
  4. historyTypeName: `Trace${size}`,
  5. checkpointTypeName: `Checkpoint${size}`,
  6. checkpointFieldName: '_checkpoints',
  7. keyTypeName: `uint${256 - size}`,
  8. keyFieldName: '_key',
  9. valueTypeName: `uint${size}`,
  10. valueFieldName: '_value',
  11. });
  12. const VALUE_SIZES = [224, 160];
  13. const OPTS = VALUE_SIZES.map(size => defaultOpts(size));
  14. const LEGACY_OPTS = {
  15. ...defaultOpts(224),
  16. historyTypeName: 'History',
  17. checkpointTypeName: 'Checkpoint',
  18. keyFieldName: '_blockNumber',
  19. };
  20. // TEMPLATE
  21. const header = `\
  22. pragma solidity ^0.8.0;
  23. import "./math/Math.sol";
  24. import "./math/SafeCast.sol";
  25. /**
  26. * @dev This library defines the \`History\` struct, for checkpointing values as they change at different points in
  27. * time, and later looking up past values by block number. See {Votes} as an example.
  28. *
  29. * To create a history of checkpoints define a variable type \`Checkpoints.History\` in your contract, and store a new
  30. * checkpoint for the current transaction block using the {push} function.
  31. *
  32. * _Available since v4.5._
  33. */
  34. `;
  35. const types = opts => `\
  36. struct ${opts.historyTypeName} {
  37. ${opts.checkpointTypeName}[] ${opts.checkpointFieldName};
  38. }
  39. struct ${opts.checkpointTypeName} {
  40. ${opts.keyTypeName} ${opts.keyFieldName};
  41. ${opts.valueTypeName} ${opts.valueFieldName};
  42. }
  43. `;
  44. /* eslint-disable max-len */
  45. const operations = opts => `\
  46. /**
  47. * @dev Pushes a (\`key\`, \`value\`) pair into a ${opts.historyTypeName} so that it is stored as the checkpoint.
  48. *
  49. * Returns previous value and new value.
  50. */
  51. function push(
  52. ${opts.historyTypeName} storage self,
  53. ${opts.keyTypeName} key,
  54. ${opts.valueTypeName} value
  55. ) internal returns (${opts.valueTypeName}, ${opts.valueTypeName}) {
  56. return _insert(self.${opts.checkpointFieldName}, key, value);
  57. }
  58. /**
  59. * @dev Returns the value in the oldest checkpoint with key greater or equal than the search key, or zero if there is none.
  60. */
  61. function lowerLookup(${opts.historyTypeName} storage self, ${opts.keyTypeName} key) internal view returns (${opts.valueTypeName}) {
  62. uint256 len = self.${opts.checkpointFieldName}.length;
  63. uint256 pos = _lowerBinaryLookup(self.${opts.checkpointFieldName}, key, 0, len);
  64. return pos == len ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos).${opts.valueFieldName};
  65. }
  66. /**
  67. * @dev Returns the value in the most recent checkpoint with key lower or equal than the search key.
  68. */
  69. function upperLookup(${opts.historyTypeName} storage self, ${opts.keyTypeName} key) internal view returns (${opts.valueTypeName}) {
  70. uint256 len = self.${opts.checkpointFieldName}.length;
  71. uint256 pos = _upperBinaryLookup(self.${opts.checkpointFieldName}, key, 0, len);
  72. return pos == 0 ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1).${opts.valueFieldName};
  73. }
  74. /**
  75. * @dev Returns the value in the most recent checkpoint with key lower or equal than the search key.
  76. *
  77. * NOTE: This is a variant of {upperLookup} that is optimised to find "recent" checkpoint (checkpoints with high keys).
  78. */
  79. function upperLookupRecent(${opts.historyTypeName} storage self, ${opts.keyTypeName} key) internal view returns (${opts.valueTypeName}) {
  80. uint256 len = self.${opts.checkpointFieldName}.length;
  81. uint256 low = 0;
  82. uint256 high = len;
  83. if (len > 5) {
  84. uint256 mid = len - Math.sqrt(len);
  85. if (key < _unsafeAccess(self.${opts.checkpointFieldName}, mid)._key) {
  86. high = mid;
  87. } else {
  88. low = mid + 1;
  89. }
  90. }
  91. uint256 pos = _upperBinaryLookup(self.${opts.checkpointFieldName}, key, low, high);
  92. return pos == 0 ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1).${opts.valueFieldName};
  93. }
  94. `;
  95. const legacyOperations = opts => `\
  96. /**
  97. * @dev Returns the value at a given block number. If a checkpoint is not available at that block, the closest one
  98. * before it is returned, or zero otherwise. Because the number returned corresponds to that at the end of the
  99. * block, the requested block number must be in the past, excluding the current block.
  100. */
  101. function getAtBlock(${opts.historyTypeName} storage self, uint256 blockNumber) internal view returns (uint256) {
  102. require(blockNumber < block.number, "Checkpoints: block not yet mined");
  103. uint32 key = SafeCast.toUint32(blockNumber);
  104. uint256 len = self.${opts.checkpointFieldName}.length;
  105. uint256 pos = _upperBinaryLookup(self.${opts.checkpointFieldName}, key, 0, len);
  106. return pos == 0 ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1).${opts.valueFieldName};
  107. }
  108. /**
  109. * @dev Returns the value at a given block number. If a checkpoint is not available at that block, the closest one
  110. * before it is returned, or zero otherwise. Similar to {upperLookup} but optimized for the case when the searched
  111. * checkpoint is probably "recent", defined as being among the last sqrt(N) checkpoints where N is the number of
  112. * checkpoints.
  113. */
  114. function getAtProbablyRecentBlock(${opts.historyTypeName} storage self, uint256 blockNumber) internal view returns (uint256) {
  115. require(blockNumber < block.number, "Checkpoints: block not yet mined");
  116. uint32 key = SafeCast.toUint32(blockNumber);
  117. uint256 len = self.${opts.checkpointFieldName}.length;
  118. uint256 low = 0;
  119. uint256 high = len;
  120. if (len > 5) {
  121. uint256 mid = len - Math.sqrt(len);
  122. if (key < _unsafeAccess(self.${opts.checkpointFieldName}, mid)._blockNumber) {
  123. high = mid;
  124. } else {
  125. low = mid + 1;
  126. }
  127. }
  128. uint256 pos = _upperBinaryLookup(self.${opts.checkpointFieldName}, key, low, high);
  129. return pos == 0 ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1).${opts.valueFieldName};
  130. }
  131. /**
  132. * @dev Pushes a value onto a History so that it is stored as the checkpoint for the current block.
  133. *
  134. * Returns previous value and new value.
  135. */
  136. function push(${opts.historyTypeName} storage self, uint256 value) internal returns (uint256, uint256) {
  137. return _insert(self.${opts.checkpointFieldName}, SafeCast.toUint32(block.number), SafeCast.toUint224(value));
  138. }
  139. /**
  140. * @dev Pushes a value onto a History, by updating the latest value using binary operation \`op\`. The new value will
  141. * be set to \`op(latest, delta)\`.
  142. *
  143. * Returns previous value and new value.
  144. */
  145. function push(
  146. ${opts.historyTypeName} storage self,
  147. function(uint256, uint256) view returns (uint256) op,
  148. uint256 delta
  149. ) internal returns (uint256, uint256) {
  150. return push(self, op(latest(self), delta));
  151. }
  152. `;
  153. const common = opts => `\
  154. /**
  155. * @dev Returns the value in the most recent checkpoint, or zero if there are no checkpoints.
  156. */
  157. function latest(${opts.historyTypeName} storage self) internal view returns (${opts.valueTypeName}) {
  158. uint256 pos = self.${opts.checkpointFieldName}.length;
  159. return pos == 0 ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1).${opts.valueFieldName};
  160. }
  161. /**
  162. * @dev Returns whether there is a checkpoint in the structure (i.e. it is not empty), and if so the key and value
  163. * in the most recent checkpoint.
  164. */
  165. function latestCheckpoint(${opts.historyTypeName} storage self)
  166. internal
  167. view
  168. returns (
  169. bool exists,
  170. ${opts.keyTypeName} ${opts.keyFieldName},
  171. ${opts.valueTypeName} ${opts.valueFieldName}
  172. )
  173. {
  174. uint256 pos = self.${opts.checkpointFieldName}.length;
  175. if (pos == 0) {
  176. return (false, 0, 0);
  177. } else {
  178. ${opts.checkpointTypeName} memory ckpt = _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1);
  179. return (true, ckpt.${opts.keyFieldName}, ckpt.${opts.valueFieldName});
  180. }
  181. }
  182. /**
  183. * @dev Returns the number of checkpoint.
  184. */
  185. function length(${opts.historyTypeName} storage self) internal view returns (uint256) {
  186. return self.${opts.checkpointFieldName}.length;
  187. }
  188. /**
  189. * @dev Pushes a (\`key\`, \`value\`) pair into an ordered list of checkpoints, either by inserting a new checkpoint,
  190. * or by updating the last one.
  191. */
  192. function _insert(
  193. ${opts.checkpointTypeName}[] storage self,
  194. ${opts.keyTypeName} key,
  195. ${opts.valueTypeName} value
  196. ) private returns (${opts.valueTypeName}, ${opts.valueTypeName}) {
  197. uint256 pos = self.length;
  198. if (pos > 0) {
  199. // Copying to memory is important here.
  200. ${opts.checkpointTypeName} memory last = _unsafeAccess(self, pos - 1);
  201. // Checkpoint keys must be non-decreasing.
  202. require(last.${opts.keyFieldName} <= key, "Checkpoint: decreasing keys");
  203. // Update or push new checkpoint
  204. if (last.${opts.keyFieldName} == key) {
  205. _unsafeAccess(self, pos - 1).${opts.valueFieldName} = value;
  206. } else {
  207. self.push(${opts.checkpointTypeName}({${opts.keyFieldName}: key, ${opts.valueFieldName}: value}));
  208. }
  209. return (last.${opts.valueFieldName}, value);
  210. } else {
  211. self.push(${opts.checkpointTypeName}({${opts.keyFieldName}: key, ${opts.valueFieldName}: value}));
  212. return (0, value);
  213. }
  214. }
  215. /**
  216. * @dev Return the index of the oldest checkpoint whose key is greater than the search key, or \`high\` if there is none.
  217. * \`low\` and \`high\` define a section where to do the search, with inclusive \`low\` and exclusive \`high\`.
  218. *
  219. * WARNING: \`high\` should not be greater than the array's length.
  220. */
  221. function _upperBinaryLookup(
  222. ${opts.checkpointTypeName}[] storage self,
  223. ${opts.keyTypeName} key,
  224. uint256 low,
  225. uint256 high
  226. ) private view returns (uint256) {
  227. while (low < high) {
  228. uint256 mid = Math.average(low, high);
  229. if (_unsafeAccess(self, mid).${opts.keyFieldName} > key) {
  230. high = mid;
  231. } else {
  232. low = mid + 1;
  233. }
  234. }
  235. return high;
  236. }
  237. /**
  238. * @dev Return the index of the oldest checkpoint whose key is greater or equal than the search key, or \`high\` if there is none.
  239. * \`low\` and \`high\` define a section where to do the search, with inclusive \`low\` and exclusive \`high\`.
  240. *
  241. * WARNING: \`high\` should not be greater than the array's length.
  242. */
  243. function _lowerBinaryLookup(
  244. ${opts.checkpointTypeName}[] storage self,
  245. ${opts.keyTypeName} key,
  246. uint256 low,
  247. uint256 high
  248. ) private view returns (uint256) {
  249. while (low < high) {
  250. uint256 mid = Math.average(low, high);
  251. if (_unsafeAccess(self, mid).${opts.keyFieldName} < key) {
  252. low = mid + 1;
  253. } else {
  254. high = mid;
  255. }
  256. }
  257. return high;
  258. }
  259. /**
  260. * @dev Access an element of the array without performing bounds check. The position is assumed to be within bounds.
  261. */
  262. function _unsafeAccess(${opts.checkpointTypeName}[] storage self, uint256 pos)
  263. private
  264. pure
  265. returns (${opts.checkpointTypeName} storage result)
  266. {
  267. assembly {
  268. mstore(0, self.slot)
  269. result.slot := add(keccak256(0, 0x20), pos)
  270. }
  271. }
  272. `;
  273. /* eslint-enable max-len */
  274. // GENERATE
  275. module.exports = format(
  276. header.trimEnd(),
  277. 'library Checkpoints {',
  278. [
  279. // Legacy types & functions
  280. types(LEGACY_OPTS),
  281. legacyOperations(LEGACY_OPTS),
  282. common(LEGACY_OPTS),
  283. // New flavors
  284. ...OPTS.flatMap(opts => [types(opts), operations(opts), common(opts)]),
  285. ],
  286. '}',
  287. );