diff --git a/foundry.toml b/foundry.toml index f84ae83..2c501a3 100644 --- a/foundry.toml +++ b/foundry.toml @@ -15,6 +15,7 @@ cbor_metadata = false gas_reports = ["*"] fs_permissions = [{ access = "read-write", path = "./broadcast" }] ignored_warnings_from = ["lib"] # ignoring warnings from external packages +allow_internal_expect_revert = true fuzz = { runs = 256 } invariant = { runs = 256, depth = 15, fail_on_revert = false, call_override = false } diff --git a/src/Multiverse.sol b/src/Multiverse.sol index 3f13838..40748a7 100644 --- a/src/Multiverse.sol +++ b/src/Multiverse.sol @@ -10,6 +10,7 @@ import { ILituusRep } from "./interfaces/ILituusRep.sol"; import { LituusRep } from "./LituusRep.sol"; import { IReputationToken } from "./interfaces/IReputationToken.sol"; import { IQueryFeeController } from "./interfaces/IQueryFeeController.sol"; +import { LibHistory } from "./libraries/LibHistory.sol"; // TODO: check zoltar forks @@ -78,10 +79,20 @@ contract Multiverse is ReentrancyGuard { ForkState forkState; // TODO: populate the forkTime uint48 forkTime; + // The depth of the universe in the fork tree + // Genesis universe has depth 0, its children have depth 1, etc. Max 256. + uint16 forkDepth; + // Whether this universe lies on the canonical timeline (the genesis -> favoriteChild -> ... chain). + // Genesis is canonical; on a fork, only the designated favoriteChild inherits the parent's flag. + bool isCanonical; uint248 parent; uint248 favoriteChild; uint248 heir; // TODO: populate the history + // History format: + // Genesis universe has history 0, depth 0. + // First children have history 0b00 and 0b01, depth 1, + // second children of the second child 0b010 and 0b011, depth 2, etc. bytes32 history; uint256 forkQuery; uint256 supplyBeforeFork; @@ -159,7 +170,10 @@ contract Multiverse is ReentrancyGuard { genesisUniverse.forkState = ForkState.NotForking; genesisUniverse.forkTime = uint48(block.timestamp); genesisUniverse.heir = 0; + // Genesis is the root of the fork tree: an empty inheritance path, and the root of the canonical timeline. genesisUniverse.history = 0; + genesisUniverse.forkDepth = 0; + genesisUniverse.isCanonical = true; genesisUniverse.forkQuery = 0; // TODO: fill in the correct supply genesisUniverse.supplyBeforeFork = ZOLTAR.getUniverseTheoreticalSupply(_initialZoltarUniverseId); @@ -182,8 +196,7 @@ contract Multiverse is ReentrancyGuard { /* ============================================= QUERY FUNCTIONS ============================================= */ function createQuery(uint248 universeId, string calldata question, uint8 numberOfOutcomes) external nonReentrant { - (uint248 activeUniverseId, Universe storage universe, ILituusRep repToken) = - _getActiveUniverseAndRepToken(universeId); + (uint248 activeUniverseId,, ILituusRep repToken) = _getActiveUniverseAndRepToken(universeId); // Validate the question and number of outcomes if (numberOfOutcomes <= 2) revert InvalidNumberOfOutcomes(); @@ -217,14 +230,19 @@ contract Multiverse is ReentrancyGuard { function report(uint248 universeId, uint256 queryId, uint8 outcome) external nonReentrant { // Check all conditions (universe exists, query exists, outcome is valid, report is within time, etc.) - (uint248 activeUniverseId, Universe storage universe, ILituusRep repToken) = - _getActiveUniverseAndRepToken(universeId); + (uint248 activeUniverseId,, ILituusRep repToken) = _getActiveUniverseAndRepToken(universeId); Query storage query = queries[queryId]; if (query.numberOfOutcomes == 0) revert InvalidQuery(); QueryResolution storage resolution = queryResolutions[activeUniverseId][queryId]; if (resolution.outcome != UNRESOLVED) revert QueryAlreadyResolved(); + // A query resolved in an ancestor universe is inherited by this lineage (via getOutcome), so it + // cannot be reported on again here. The ancestor set is fixed per lineage, so this only needs to + // be checked on the first report; later stakes in the same escalation are already covered. + if (resolution.stakes.length == 0 && _findAncestorResolution(activeUniverseId, queryId) != UNRESOLVED) { + revert QueryAlreadyResolved(); + } // Outcome should be between 1 and numberOfOutcomes unless the query should be reported as INVALID if (outcome == UNRESOLVED) revert InvalidOutcome(); @@ -270,8 +288,7 @@ contract Multiverse is ReentrancyGuard { } function resolve(uint248 universeId, uint256 queryId) external nonReentrant { - (uint248 activeUniverseId, Universe storage universe, ILituusRep repToken) = - _getActiveUniverseAndRepToken(universeId); + (uint248 activeUniverseId, Universe storage activeUniverse,) = _getActiveUniverseAndRepToken(universeId); Query storage query = queries[queryId]; if (query.numberOfOutcomes == 0) revert InvalidQuery(); @@ -282,8 +299,13 @@ contract Multiverse is ReentrancyGuard { uint48 queryCreateTime = _getAndUpdateQueryCreateTime(activeUniverseId, queryId); if (resolution.stakes.length == 0 && queryCreateTime + THREE_DAYS < block.timestamp) { + // No report ever landed here, so the ancestor check was never run by report(): a query + // resolved in an ancestor is inherited by this lineage (via getOutcome) and must not be + // resolved again. The stakes branch below is already covered by report()'s first-stake check. + if (_findAncestorResolution(activeUniverseId, queryId) != UNRESOLVED) revert QueryAlreadyResolved(); // If the report period has passed and the query was not reported on then resolve the query as INVALID resolution.outcome = INVALID; + _recordResolvedUniverse(queryId, activeUniverseId, activeUniverse.isCanonical); emit QueryResolved(msg.sender, activeUniverseId, queryId, INVALID); // TODO: Payout to the resolver, another clock auction } else if (resolution.stakes.length > 0) { @@ -292,6 +314,7 @@ contract Multiverse is ReentrancyGuard { // TODO: Unless the query is 1 step from fork threshold, then we should wait for the fork to finish uint8 outcome = _calculateOutcomeAndEscalationPayoffs(activeUniverseId, queryId); resolution.outcome = outcome; + _recordResolvedUniverse(queryId, activeUniverseId, activeUniverse.isCanonical); emit QueryResolved(msg.sender, activeUniverseId, queryId, outcome); // TODO: Payouts } else { @@ -465,9 +488,73 @@ contract Multiverse is ReentrancyGuard { } /* ========================================= PUBLIC VIEW FUNCTIONS =========================================== */ + /** + * @notice Returns the outcome of a query as seen from a given universe. + * @dev If the query is resolved in this universe, that outcome is returned directly. Otherwise the + * query's resolutions are scanned for one recorded in an ancestor of this universe (a prefix + * match on the inheritance path). A query is resolved at most once along any single lineage, + * so the first ancestor match is the applicable resolution. Returns UNRESOLVED if neither this + * universe nor any ancestor has resolved the query. + * @param universeId The universe to read the outcome from. + * @param queryId The query to read. + * @return The resolved outcome, or UNRESOLVED if none applies to this universe. + */ function getOutcome(uint248 universeId, uint256 queryId) external view returns (uint8) { - // TODO: outcome lookup in ancestor universes if unresolved in the current universe - return queryResolutions[universeId][queryId].outcome; + // Fast path: resolved in this universe. + uint8 localOutcome = queryResolutions[universeId][queryId].outcome; + if (localOutcome != UNRESOLVED) return localOutcome; + + // Forward to the heir if this universe has forked, so we read from the active universe where + // report()/resolve() record resolutions. Reverts only if the universe does not exist; outcomes + // remain readable while the universe is forking (no fork-state check, unlike report/resolve). + (uint248 activeUniverseId,) = _getActiveUniverse(universeId); + if (activeUniverseId != universeId) { + uint8 heirOutcome = queryResolutions[activeUniverseId][queryId].outcome; + if (heirOutcome != UNRESOLVED) return heirOutcome; + } + + // Otherwise inherit the resolution from an ancestor universe, if any. + return _findAncestorResolution(activeUniverseId, queryId); + } + + /** + * @notice Returns the outcome of the first ancestor of `universeId` that has resolved `queryId`. + * @dev Scans the query's recorded resolution universes and prefix-matches their inheritance path + * against `universeId`'s path. A query is resolved at most once along any single + * lineage, so the first ancestor match is authoritative. Returns UNRESOLVED if no ancestor + * has resolved the query. + */ + function _findAncestorResolution(uint248 universeId, uint256 queryId) internal view returns (uint8) { + Universe storage universe = universes[universeId]; + bytes32 history = universe.history; + uint16 forkDepth = universe.forkDepth; + + uint248[] storage resolvedUniverses = queries[queryId].resolvedUniverses; + uint256 length = resolvedUniverses.length; + for (uint256 i = 0; i < length; i++) { + uint248 resolvedUniverseId = resolvedUniverses[i]; + Universe storage candidate = universes[resolvedUniverseId]; + if (LibHistory.isAncestor(candidate.history, candidate.forkDepth, history, forkDepth)) { + return queryResolutions[resolvedUniverseId][queryId].outcome; + } + } + return UNRESOLVED; + } + + /** + * @notice Records `universeId` as a universe where `queryId` is resolved. + * @dev Keeps a canonical universe's entry at index 0 so `_findAncestorResolution` finds the canonical + * resolution first (the common-case read). Safe because the canonical chain is a single linear + * path, so at most one canonical universe ever resolves a given query. Saves gas during lookups. + */ + function _recordResolvedUniverse(uint256 queryId, uint248 universeId, bool isCanonical) internal { + uint248[] storage resolvedUniverses = queries[queryId].resolvedUniverses; + if (isCanonical && resolvedUniverses.length > 0) { + resolvedUniverses.push(resolvedUniverses[0]); // move current head to the tail + resolvedUniverses[0] = universeId; // canonical entry takes index 0 + } else { + resolvedUniverses.push(universeId); + } } function _requiredStakeAmount(uint248 universeId, uint256 queryId) public view returns (uint256) { @@ -507,10 +594,17 @@ contract Multiverse is ReentrancyGuard { } /* =========================================== INTERNAL HELPERS ============================================== */ - function _getActiveUniverseAndRepToken(uint248 universeId) + /** + * @notice Resolves a universe id to the active universe, forwarding to the heir if it has forked. + * @dev Reverts with InvalidUniverse only if the universe (or its heir) does not exist (repToken == 0). + * Does NOT check the fork state, so callers that must operate only on an active/forming universe + * (report/resolve) layer that check on top; read-only callers (getOutcome) can use this directly + * to stay readable while a universe is forking. + */ + function _getActiveUniverse(uint248 universeId) internal view - returns (uint248 activeUniverseId, Universe storage universe, ILituusRep repToken) + returns (uint248 activeUniverseId, Universe storage universe) { universe = universes[universeId]; if (address(universe.repToken) == address(0)) revert InvalidUniverse(); @@ -524,6 +618,14 @@ contract Multiverse is ReentrancyGuard { } else { activeUniverseId = universeId; } + } + + function _getActiveUniverseAndRepToken(uint248 universeId) + internal + view + returns (uint248 activeUniverseId, Universe storage universe, ILituusRep repToken) + { + (activeUniverseId, universe) = _getActiveUniverse(universeId); // Sanity check: if the universe is not active or forming, // then the reporting should have been forwarded to the heir. ForkState forkState = universe.forkState; @@ -541,6 +643,7 @@ contract Multiverse is ReentrancyGuard { if (resolution.queryCreateTime != 0) { return resolution.queryCreateTime; } else { + // TODO: check query flow after forks // If the queryCreateTime is not set, it means the query was not created in this universe but was reported // on in this universe. In this case, we should use the forkTime of the universe as the queryCreateTime, // since the query becomes reportable in this universe after the fork. diff --git a/src/libraries/LibHistory.sol b/src/libraries/LibHistory.sol new file mode 100644 index 0000000..5e9f978 --- /dev/null +++ b/src/libraries/LibHistory.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.35; + +/** + * @title LibHistory + * @notice Encodes and queries a universe's path of inheritance through the fork tree as a bitmap, + * enabling O(1) ancestor checks via prefix matching. + * @dev Encoding is MSB-first / left-aligned. The genesis universe has `history == 0` and + * `forkDepth == 0`. Each binary fork appends one bit (the branch taken, 0 or 1) to the + * path; the bit for fork depth `i` (1-based because depth 0 is the genesis) lives at bit + * `256 - i`, so the oldest fork is the most-significant bit (i=1 -> bit 255, i=256 -> bit 0). + * A path of depth `d` therefore occupies the top `d` bits and the remaining `256 - d` + * low bits MUST be zero. + * + * `forkDepth` is required to disambiguate paths: a branch-0 fork sets no bit, so a history + * alone cannot distinguish e.g. "0" (depth 1) from genesis (depth 0) — the depth carries + * that information. Ancestry is "A is an ancestor-or-equal of B if A's path is a prefix of + * B's path", which reduces to comparing the top `A.forkDepth` bits. + * + * All functions are `pure` and operate only on their arguments. Inputs with a depth above + * {MAX_FORK_DEPTH}) revert rather than returning a value: this is a sanity check that + * should never trigger for histories built by the system through {appendHistory}. + * + * Bits below the declared depth MUST be zero, but this is not enforced by the library: it is the caller's + * responsibility to ensure that the history is well-formed. The library does not have a validation + * for this because of gas optimization. It is intended to be used by the Lituus Multiverse contract, + * which maintains a consistent structure of universe histories. + */ +library LibHistory { + /// @notice Maximum fork depth. A path can hold at most 256 single-bit fork branches in a bytes32. + uint256 public constant MAX_FORK_DEPTH = 256; + + /// @notice Thrown when a fork depth exceeds {MAX_FORK_DEPTH} (or would on append). + error ForkDepthOverflow(); + /// @notice Thrown when a fork outcome index is not a valid binary branch (must be 0 or 1). + error InvalidForkOutcomeIndex(); + + /** + * @notice Appends a fork branch to a universe's path, producing the child universe's history. + * @dev Reverts with {ForkDepthOverflow} if the parent is at or above {MAX_FORK_DEPTH} and it's + * impossible to append a value, and {InvalidForkOutcomeIndex} if `forkOutcomeIndex > 1`. + * The new branch bit is placed at bit `256 - newForkDepth` (MSB-first); a branch of 0 + * leaves the bitmap unchanged and is distinguished only by the incremented depth. + * @param history The parent universe's history bitmap. + * @param forkDepth The parent universe's fork depth (number of branches in `history`). + * @param forkOutcomeIndex The branch taken by the child: 0 or 1. + * @return newHistory The child universe's history bitmap. + * @return newForkDepth The child universe's fork depth (`forkDepth + 1`). + */ + function appendHistory(bytes32 history, uint16 forkDepth, uint8 forkOutcomeIndex) + internal + pure + returns (bytes32 newHistory, uint16 newForkDepth) + { + // Provided fork depth should be strictly less than MAX_FORK_DEPTH, because the new depth will be forkDepth + 1. + if ((forkDepth >= MAX_FORK_DEPTH)) { + revert ForkDepthOverflow(); + } + // Only two branches are valid for the binary fork: 0 or 1. + if (forkOutcomeIndex > 1) { + revert InvalidForkOutcomeIndex(); + } + newForkDepth = forkDepth + 1; + newHistory = history | bytes32(uint256(forkOutcomeIndex) << (MAX_FORK_DEPTH - newForkDepth)); + } + + /** + * @notice Returns whether one universe is an ancestor of (or equal to) another, i.e. whether the + * ancestor's path is a prefix of the descendant's path. + * @dev O(1): compares the top `ancestorForkDepth` bits of both histories. The genesis universe + * (depth 0) is an ancestor of every universe. Reverts with {ForkDepthOverflow} if either + * depth exceeds {MAX_FORK_DEPTH}. + * @param ancestorHistory The candidate ancestor's history bitmap. + * @param ancestorForkDepth The candidate ancestor's fork depth. + * @param descendantHistory The candidate descendant's history bitmap. + * @param descendantForkDepth The candidate descendant's fork depth. + * @return True if the ancestor's path is a prefix of the descendant's path (equal paths included). + */ + function isAncestor( + bytes32 ancestorHistory, + uint16 ancestorForkDepth, + bytes32 descendantHistory, + uint16 descendantForkDepth + ) internal pure returns (bool) { + if ((ancestorForkDepth > MAX_FORK_DEPTH) || (descendantForkDepth > MAX_FORK_DEPTH)) { + revert ForkDepthOverflow(); + } + // A deeper path can never be a prefix of a shallower one. + if (ancestorForkDepth > descendantForkDepth) { + return false; + } + // Compare the top `ancestorForkDepth` bits. At depth 0 the mask is 0 (shift by 256), + // so genesis (history 0) matches every descendant. + // The shift is the number of low bits to clear, so the mask is all 1s in the top `ancestorForkDepth` bits and + // 0s below. + uint256 shift = MAX_FORK_DEPTH - ancestorForkDepth; + // The mask is 1s in the top `ancestorForkDepth` bits and 0s below (to get the prefix for comparison). + uint256 mask = uint256(type(uint256).max) << shift; + // ANDing the mask with the descendant history clears all bits below that depth. + // Compare the masked descendant history to the ancestor history: + // if they are equal, the ancestor's path is a prefix of the descendant's path. + // Example: ancestor depth 3, history 010... (top 3 bits), descendant depth 5, history 01010... (top 5 bits). + // shift = 256 - 3 = 253, mask = 111000...0 (top 3 bits are set to 1, the rest are set to 0) + // descendantHistory & mask = 01010... & 11100.... = 01000..., this prefix equals ancestorHistory. + return ancestorHistory == (descendantHistory & bytes32(mask)); + } +} diff --git a/test/unit/LibHistory.t.sol b/test/unit/LibHistory.t.sol new file mode 100644 index 0000000..1e07635 --- /dev/null +++ b/test/unit/LibHistory.t.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.35; + +import { Test } from "forge-std/Test.sol"; +import { LibHistory } from "src/libraries/LibHistory.sol"; + +// Test for the LibHistory library (path-of-inheritance bitmap). +// +// Encoding: MSB-first / left-aligned single-bit path. +// + +contract LibHistoryTest is Test { + /* ----------------------------------- appendHistory ---------------------------------- */ + + /// genesis (history 0, depth 0) -> child on branch 0 and branch 1 produce distinct (history, depth). + function test_appendHistory_genesisChildren_distinct() public pure { + (bytes32 h0, uint16 d0) = LibHistory.appendHistory(bytes32(0), 0, 0); + (bytes32 h1, uint16 d1) = LibHistory.appendHistory(bytes32(0), 0, 1); + assertEq(d0, 1); + assertEq(d1, 1); + assertEq(h0, 0x0000000000000000000000000000000000000000000000000000000000000000); // 0.... + assertEq(h1, 0x8000000000000000000000000000000000000000000000000000000000000000); // 1.... + assertTrue(h0 != h1); + } + + /// chained appends 0->1->1->0->1->0 build the path "011010" at the top 6 bits + function test_appendHistory_chainedPath() public pure { + // build path b0..b5 = 0,1,1,0,1,0 ; expect history top byte == 0x68, depth == 6, + // and (history >> (256 - 6)) == 0b011010 (== 26). + bytes32 history = bytes32(0); // genesis + uint16 depth = 0; // genesis + (history, depth) = LibHistory.appendHistory(history, depth, 0); // 0 + (history, depth) = LibHistory.appendHistory(history, depth, 1); // 01 + (history, depth) = LibHistory.appendHistory(history, depth, 1); // 011 + (history, depth) = LibHistory.appendHistory(history, depth, 0); // 0110 + (history, depth) = LibHistory.appendHistory(history, depth, 1); // 01101 + (history, depth) = LibHistory.appendHistory(history, depth, 0); // 011010 + assertEq(depth, 6); + assertEq(history, 0x6800000000000000000000000000000000000000000000000000000000000000); // 011010..... + } + + /// appends history when the MAX_FORK_DEPTH is not exceeded. + function test_appendHistory_atMaxDepth() public pure { + bytes32 history = bytes32(0); + uint16 depth = 255; + (history, depth) = LibHistory.appendHistory(history, depth, 0); + assertEq(depth, 256); + } + + /// reverts when parentDepth >= MAX_FORK_DEPTH (256). + function test_appendHistory_revertsAtMaxDepth() public { + vm.expectRevert(LibHistory.ForkDepthOverflow.selector); + LibHistory.appendHistory(bytes32(0), 256, 0); + } + + /// reverts for a non-binary branch value (b > 1). + function test_appendHistory_revertsOnInvalidBranch() public { + vm.expectRevert(LibHistory.InvalidForkOutcomeIndex.selector); + LibHistory.appendHistory(bytes32(0), 0, 2); + } + + /* ----------------------------------- isAncestor --------------------------------- */ + + /// genesis (depth 0) is an ancestor of every universe, including itself. + function test_isAncestor_genesisIsAncestorOfAll() public pure { + bytes32 anyHistory = 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef; + uint16 anyDepth = 256; + assertTrue(LibHistory.isAncestor(bytes32(0), 0, anyHistory, anyDepth)); + bytes32 otherHistory = 0x1200000000000000000000000000000000000000000000000000000000000000; + uint16 otherDepth = 8; + assertTrue(LibHistory.isAncestor(bytes32(0), 0, otherHistory, otherDepth)); + // Check genesis is ancestor of itself + assertTrue(LibHistory.isAncestor(bytes32(0), 0, bytes32(0), 0)); + } + + /// "0110" is an ancestor of "011010", but "0111" is not. + function test_isAncestor_prefixMatchAndMismatch() public pure { + // anc0110 (depth 4), desc011010 (depth 6) -> true + bytes32 ancestorHistory = 0x6000000000000000000000000000000000000000000000000000000000000000; // 0110.... + uint16 ancestorDepth = 4; + bytes32 descendantHistory = 0x6800000000000000000000000000000000000000000000000000000000000000; // 011010... + uint16 descendantDepth = 6; + assertTrue(LibHistory.isAncestor(ancestorHistory, ancestorDepth, descendantHistory, descendantDepth)); + + // anc0111 (depth 4), desc011010 (depth 6) -> false + ancestorHistory = 0x7000000000000000000000000000000000000000000000000000000000000000; // 0111.... + ancestorDepth = 4; + descendantHistory = 0x6800000000000000000000000000000000000000000000000000000000000000; // 011010... + descendantDepth = 6; + assertFalse(LibHistory.isAncestor(ancestorHistory, ancestorDepth, descendantHistory, descendantDepth)); + } + + /// equal paths are ancestor-or-equal (true). + function test_isAncestor_equalPathsAreAncestors() public pure { + bytes32 history = 0x6000000000000000000000000000000000000000000000000000000000000000; // 0110.... + uint16 depth = 4; + assertTrue(LibHistory.isAncestor(history, depth, history, depth)); + } + + /// a sibling is not an ancestor (false). + function test_isAncestor_siblingIsNotAncestor() public pure { + bytes32 ancestorHistory = 0x6800000000000000000000000000000000000000000000000000000000000000; // 0110.... + uint16 ancestorDepth = 6; + bytes32 descendantHistory = 0x6C00000000000000000000000000000000000000000000000000000000000000; // 0110.... + uint16 descendantDepth = 6; + assertFalse(LibHistory.isAncestor(ancestorHistory, ancestorDepth, descendantHistory, descendantDepth)); + } + + /// a deeper "ancestor" than the descendant can never be an ancestor (false). + function test_isAncestor_deeperThanDescendantIsFalse() public pure { + bytes32 ancestorHistory = 0x6000000000000000000000000000000000000000000000000000000000000000; // 0110.... + uint16 ancestorDepth = 6; + bytes32 descendantHistory = 0x6000000000000000000000000000000000000000000000000000000000000000; // 0110.... + uint16 descendantDepth = 4; + assertFalse(LibHistory.isAncestor(ancestorHistory, ancestorDepth, descendantHistory, descendantDepth)); + } + + /// boundary: ancestorDepth == 256 (full word) compares the entire path without reverting. + function test_isAncestor_depth256Boundary() public pure { + bytes32 ancestorHistory = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; // 256 bits + uint16 ancestorDepth = 256; + bytes32 descendantHistory = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; // 256 bits + uint16 descendantDepth = 256; + assertTrue(LibHistory.isAncestor(ancestorHistory, ancestorDepth, descendantHistory, descendantDepth)); + } + + /// all-zero-branch universes are distinguished only by depth: "00" is an ancestor of "00000", + /// the reverse is not, and "000" is not an ancestor of the same-depth "001". + function test_isAncestor_allZeroBranchesDisambiguatedByDepth() public pure { + // "00" (depth 2) is an ancestor of "00000" (depth 5) + assertTrue(LibHistory.isAncestor(bytes32(0), 2, bytes32(0), 5)); + // the deeper all-zero path is not an ancestor of the shorter one + assertFalse(LibHistory.isAncestor(bytes32(0), 5, bytes32(0), 2)); + // "000" is not an ancestor of the same-depth sibling "001" + bytes32 oneAtThird = 0x2000000000000000000000000000000000000000000000000000000000000000; // 001.... + assertFalse(LibHistory.isAncestor(bytes32(0), 3, oneAtThird, 3)); + } + + /* ------------------------------------- fuzz ------------------------------------- */ + + /// a freshly-appended child always has its parent as an ancestor. + function testFuzz_appendThenAncestor(bytes32 parentHistory, uint16 parentDepth, bool branch) public pure { + parentDepth = uint16(bound(parentDepth, 0, 255)); + parentHistory = _clearHistoryAfterDepth(parentHistory, parentDepth); + (bytes32 childHistory, uint16 childDepth) = LibHistory.appendHistory(parentHistory, parentDepth, branch ? 1 : 0); + assertTrue(LibHistory.isAncestor(parentHistory, parentDepth, childHistory, childDepth)); + } + + function _clearHistoryAfterDepth(bytes32 history, uint16 depth) internal pure returns (bytes32) { + uint256 shift = 256 - depth; + uint256 mask = uint256(type(uint256).max) << shift; + return history & bytes32(mask); + } +}