From 30f8f28aa555f64afda8c695aed1626da8ad54c8 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Mon, 27 Apr 2026 16:16:03 -0700 Subject: [PATCH 01/10] wip --- OPT_PLAN.md | 313 +++++++++++++++++++++ snapshots/StandardAttackPvPGasTest.json | 10 +- test/BatchInstrumentationTest.sol | 352 ++++++++++++++++++++++++ 3 files changed, 670 insertions(+), 5 deletions(-) create mode 100644 OPT_PLAN.md create mode 100644 test/BatchInstrumentationTest.sol diff --git a/OPT_PLAN.md b/OPT_PLAN.md new file mode 100644 index 00000000..1d6a56c3 --- /dev/null +++ b/OPT_PLAN.md @@ -0,0 +1,313 @@ +# OPT_PLAN — Batched Execute Gas Optimization + +## 1. Goal + +Amortize per-turn cold-storage access in `Engine.execute()` by: +1. Submitting each turn's signed moves on-chain immediately to a per-turn buffer (no execute). +2. Executing `N` buffered turns in one tx with engine state held in **transient shadow storage**, flushed to persistent storage once at the end. + +Secondary goal: route `Engine` state access through helpers so the single-turn path can also use the shadow layer. + +--- + +## 2. Mechanism + +### 2.1 Per-turn submission (PvP) + +`SignedCommitManager.submitTurnMoves(battleKey, TurnSubmission entry)`: +- Uniform shape every turn: one revealer EIP-712 signature, committer preimage in calldata. Roles derived from `turnId % 2` (matching `getCommitAuthForDualSigned`). +- Switch turns use the same shape. The non-acting player signs a `NO_OP` (move 126); engine ignores their half at batch time using the live `playerSwitchForTurnFlag`. +- Manager hashes committer preimage, verifies revealer sig over `DualSignedReveal`, writes to `moveBuffer[storageKey][turnId]`. **No execute runs.** +- Updates `lastSubmitTimestamp` for timeout tracking. + +### 2.2 Per-batch execute + +`Engine.executeBatch(battleKey, numTurns)`: +- Anyone can call (sigs were checked at submission). +- Reads buffered entries `[BattleData.turnId, BattleData.turnId + numTurns)`, runs each in sequence inside transient shadow storage, flushes once at end. +- `BattleData.turnId` advances inside the loop; next batch starts at the right slot. +- Processed buffer slots are not cleared — the unbounded mapping leaves them for on-chain replay. Slot reuse across battles comes from `MappingAllocator`. + +### 2.3 Fallback / stalls + +Fully separate write paths. Legacy `DefaultCommitManager.commitMove`/`revealMove` writes `config.p0Move` etc. and triggers `execute()` immediately; the batched path never reads that storage. A battle can alternate between modes turn-by-turn. Timeout via `Engine.end()` covers full stalls. + +--- + +## 3. Buffer layout + +One 256-bit slot per turn: + +```solidity +// [ p0MoveIndex (8) | p0ExtraData (16) | p0Salt (104) | p1MoveIndex (8) | p1ExtraData (16) | p1Salt (104) ] +struct PackedTurnEntry { + uint8 p0MoveIndex; + uint16 p0ExtraData; + uint104 p0Salt; + uint8 p1MoveIndex; + uint16 p1ExtraData; + uint104 p1Salt; +} + +mapping(bytes32 storageKey => mapping(uint64 turnId => PackedTurnEntry)) moveBuffer; +``` + +Steady-state cost per turn: 1 SSTORE (5k, nonzero→nonzero from prior battle's slot reuse) + 1 SLOAD inside batch (2.1k) = ~7.1k. + +**Width changes (clean break):** +- `extraData`: 240 → 16 bits. Audit confirmed all production consumers read ≤8 bits. Narrow `IMoveSet.move()`'s `extraData` param to `uint16`; repack test helpers (`_packStatBoost`, `StatBoostsMove` mock). +- `Salt`: 256 → 104 bits. 2^104 brute-force resistance is sufficient for the seconds-to-minutes commit-reveal window. + +--- + +## 4. API + +### 4.1 Submission + +```solidity +struct TurnSubmission { + uint64 turnId; + // Committer preimage: + uint8 committerMoveIndex; + uint16 committerExtraData; + uint104 committerSalt; + // Revealer reveal: + uint8 revealerMoveIndex; + uint16 revealerExtraData; + uint104 revealerSalt; + bytes sig; // revealer EIP-712 over DualSignedReveal +} + +struct DualSignedReveal { + bytes32 battleKey; + uint64 turnId; + bytes32 committerMoveHash; // keccak(committerMoveIndex, committerSalt, committerExtraData) + uint8 revealerMoveIndex; + uint16 revealerExtraData; + uint104 revealerSalt; +} + +function submitTurnMoves(bytes32 battleKey, TurnSubmission calldata entry) external; +``` + +Manager flow: +1. Battle is in dual-signed mode and not over. +2. `entry.turnId` equals next append position. +3. Derive `(committer, revealer)` from `turnId % 2`. +4. `committerMoveHash = keccak(committerMoveIndex, committerSalt, committerExtraData)`. +5. Recover signer over `DualSignedReveal`; require equality with `revealer`. +6. Map fields to `(p0, p1)` by parity; SSTORE `PackedTurnEntry`. + +### 4.2 Batch execute + +```solidity +function executeBatch(bytes32 battleKey, uint64 numTurns) external; +``` + +1. Read `startTurn = BattleData.turnId`; require turns `[startTurn, startTurn+numTurns)` all buffered. +2. Hydrate shadow. +3. For each turn: read buffer slot, populate per-turn move/salt transient, run `_executeOneTurn()`, break on game-over. +4. Flush shadow → storage. + +--- + +## 5. Transient shadow storage + +### 5.1 Shadowed state + +| Storage | Shadow form | +|---|---| +| `MonState` (per mon) | Per-`(playerIndex, monIndex)` mirror, lazy-loaded. Dirty bit per slot. | +| `koBitmaps` (16 bits in `BattleConfig` slot 2) | `uint16` mirror, loaded flag. | +| `winnerIndex` / `prevPlayerSwitchForTurnFlag` / `playerSwitchForTurnFlag` / `activeMonIndex` / `turnId` / `lastExecuteTimestamp` | Single packed `uint256` mirror. | +| Effect data slots (`globalEffects[i].data`, `pXEffects[i].data`) | Sparse transient map keyed by slot index, mirrors `data` only. `effect`/`stepsBitmap` read from storage (warm after first hit). | +| `packedP0EffectsCount` / `packedP1EffectsCount` / `globalEffectsLength` | Three small mirrors, flushed with effect-list shadow. | +| `globalKV[storageKey][key]` | Per-`key` mirror, lazy-loaded. | +| `BattleConfig.p0Move` / `p1Move` / salts | Re-populated per sub-turn from buffer slot. | + +Hydrate strategy: +- **Eager**: `BattleData` slot 1 + `BattleConfig` slot 2 (always touched). +- **Lazy**: `MonState`, effect data, `globalKV` (sparse — pay only for slots touched). + +Loaded-flag strategy: +- **Bitmap** for fixed-shape slots (MonState, effects, slot-2 packed fields). +- **Per-key transient hash-set** for `globalKV` (dynamic keys). + +### 5.2 Helper boundary + +Mirrored helpers in `Engine.sol`: + +```solidity +function _shadowReadMonState(BattleConfig storage cfg, uint256 playerIndex, uint256 monIndex) internal returns (MonState memory); +function _shadowWriteMonState(uint256 playerIndex, uint256 monIndex, MonState memory state) internal; +function _shadowReadKV(bytes32 storageKey, uint64 key) internal returns (uint192); +function _shadowWriteKV(bytes32 storageKey, uint64 key, uint192 value) internal; +function _shadowReadEffectData(uint256 effectList, uint256 monIndex, uint256 slotIndex) internal returns (bytes32); +function _shadowWriteEffectData(uint256 effectList, uint256 monIndex, uint256 slotIndex, bytes32 data) internal; +``` + +When `_shadowActive == false`, helpers SLOAD/SSTORE storage directly. When `true`, they read/write the transient mirror with lazy-load and dirty-bit bookkeeping. + +External `IEngine` writers (`updateMonState`, `dealDamage`, `addEffect`, `removeEffect`, `editEffect`, `setGlobalKV`, `switchActiveMon`, `dispatchStandardAttack`, `setMove`) and external readers (`getMonStateForBattle`, `getEffects`, `getGlobalKV`, etc.) all route through these helpers. The `battleKeyForWrite != bytes32(0)` gate stays. + +### 5.3 Batch loop + +``` +executeBatch(battleKey, numTurns): + storageKey = _getStorageKey(battleKey) + storageKeyForWrite = storageKey + battleKeyForWrite = battleKey + _shadowActive = true + + _hydrateBattleData(battleKey) + _hydrateConfigSlot2(storageKey) + + startTurn = BattleData.turnId + for t in [startTurn .. startTurn + numTurns): + bufferEntry = _readMoveBufferSlot(storageKey, t) + _populateTurnMoveTransient(bufferEntry) + _executeOneTurn() + if winnerIndex != 2: break + _resetPerTurnTransients() + + _flushBattleData(battleKey) + _flushConfigSlot2(storageKey) + _flushDirtyMonStates(storageKey) + _flushDirtyEffectData(storageKey) + _flushDirtyGlobalKV(storageKey) + + _shadowActive = false +``` + +Per sub-turn, `tempRNG = keccak(p0Salt, p1Salt)` (or single signed salt for switch turns). Engine hooks (`onRoundStart`, `onRoundEnd`) fire per sub-turn and read shadow state via the routed getters. + +--- + +## 6. Forced switches and game-over + +### 6.1 Forced switch (KO without game-over) + +Both players sign for every turn. The non-acting player signs `NO_OP`. At batch time, the engine reads the live `playerSwitchForTurnFlag` (cheap — in shadow state) and dispatches: +- `flag == 2`: process both halves. +- `flag == 0`: process p0 only, ignore p1's NO_OP. +- `flag == 1`: mirror. + +A player who maliciously signs a non-NO_OP on a turn they shouldn't act has bound themselves cryptographically, but the engine ignores the move. A player who refuses to sign stalls the batched flow; legacy single-turn paths remain as fallback. + +Submission validates only cheap invariants (battle exists, not over at last flush, append position, sig). It does **not** project `playerSwitchForTurnFlag`, since that would require replaying every unprocessed turn. + +### 6.2 Game-over mid-batch + +`_executeInternal` already breaks when `winnerIndex != 2`. Same check stops the batch loop. Remaining buffered entries stay untouched. + +### 6.3 Status-induced skip-turn + +`shouldSkipTurn` already auto-clears in `_handleMove`. No special batch handling. + +--- + +## 7. CPU mode (trusted-state batched) + +Same per-turn buffer + `executeBatch` as PvP. CPU manager packs `(Alice move, computed CPU move)` into the same `PackedTurnEntry` layout. **Zero engine changes.** + +### 7.1 Trusted state hint + +Alice supplies the projected post-prior-turn `CPUContext` in calldata. Not verified. Lying never benefits Alice — it makes the CPU's chosen move suboptimal against her, which she absorbs. This replaces the dozen-plus cold SLOADs `engine.getCPUContext(battleKey)` does today with a single calldata struct. + +### 7.2 No signature + +Alice calls directly from her wallet. Manager checks `msg.sender == alice` (same as today's `CPUMoveManager.selectMove`). The tx is the proof — no relay path needed for a single-human flow. + +### 7.3 Off-chain protocol + +Each turn, locally on Alice's client: +1. Hold current `CPUContext`-shaped state. Turn 0 = post-`startBattle` state; later turns = output of last local sim. +2. Pick Alice's move. +3. Run the transpiled engine locally to produce the post-turn state, used as next turn's hint. +4. Submit on-chain with the **current-turn** hint. + +### 7.4 Submission + +```solidity +function selectMoveWithStateHint( + bytes32 battleKey, + uint8 aliceMoveIndex, + uint16 aliceExtraData, + uint104 aliceSalt, + CPUContext calldata projectedState +) external; +``` + +1. Read current `turnId` from `BattleData`. +2. Require `msg.sender == alice`. +3. Route on `projectedState.playerSwitchForTurnFlag` (single-player vs two-player CPU branch). +4. `ICPU(cpuAddr).calculateMove(projectedState, aliceMoveIndex, aliceExtraData)` → `(cpuMove, cpuExtra)`. CPU reads from calldata only. +5. Derive CPU salt: `uint104(uint256(keccak256(abi.encode(block.timestamp, aliceSalt, turnId))))`. Emit `CPUTurnSalt(battleKey, turnId, timestamp)` so off-chain replay can reconstruct it. `turnId` in the hash prevents collision when Alice submits multiple CPU turns in the same block. +6. Pack into `PackedTurnEntry` and SSTORE into `moveBuffer[storageKey][turnId]`. + +`executeBatch` is shared with PvP — the engine doesn't know whether the buffer came from PvP or CPU submissions. + +### 7.5 Coexistence + +Battles select via the `moveManager` they're started with: +- `signedCommitManager` (extended) → PvP batched +- `cpuMoveManager` (extended) → CPU batched +- Today's unmodified managers → legacy single-turn paths + +Today's `CPUMoveManager.selectMove` stays callable for any battle that doesn't opt into batching. + +--- + +## 8. Migration + +Add new entry points alongside existing ones. No "batch mode" flag on a battle — `executeBatch` works on any battle that has buffered turns. + +Touched contracts: +- `Engine.sol`: `executeBatch` + shadow-transient layer + helper routing + flag-based per-turn dispatch. +- `IEngine.sol`: new function signatures. +- `SignedCommitManager.sol`: `submitTurnMoves` (sharing existing EIP-712 domain). +- `CPUMoveManager.sol`: `selectMoveWithStateHint`. +- `IMoveSet.sol`: narrow `extraData` to `uint16`. ~40 mon files take mechanical edits. + +Validator/legality is unchanged: signature recovery proves player intent (or `msg.sender == alice` for CPU); state-dependent illegality silently no-ops in `_handleMove`. Timeout reads `lastSubmitTimestamp` and `lastExecuteTimestamp` — whichever is more recent. + +--- + +## 9. Phased rollout + +**Phase 0 — Instrumentation refresh.** `test/BatchInstrumentationTest.sol` already wires `vm.startStateDiffRecording` for the clean damage-trade case. Add scenarios: effect-heavy turn (status DOT + StatBoosts active), forced-switch turn, multi-mon turn. Lock final batch-size guidance. + +**Phase 0.5 — Helper extraction (no behavior change).** Replace direct `MonState`/`globalKV`/effect-data SLOAD/SSTORE in `Engine.sol` with §5.2 helpers, with `_shadowActive` permanently `false`. Snapshot diff should be roughly flat. + +**Phase 1 — Single-turn shadow.** Implement transient mirrors + lazy-load/dirty-flag bookkeeping. Wire helpers to consult `_shadowActive`. Add `executeShadowed(bytes32 battleKey)` that does `execute()`'s work inside the shadow layer (hydrate → run one turn → flush). Existing test suite should pass against it. B=1 will be slightly *worse* than today's `execute()` due to bookkeeping overhead; expected. + +**Phase 2 — PvP per-turn submission + batch execute.** Extend `SignedCommitManager` with `submitTurnMoves`. Add per-turn move buffer mapping. Add `Engine.executeBatch` with flag-based dispatch (§6.1). Equivalence tests + gas snapshots. + +**Phase 2.5 — CPU mode.** Extend `CPUMoveManager` with `selectMoveWithStateHint` (§7.4). Reuse Phase-2 buffer + `executeBatch`. Equivalence test: 24-turn CPU game via legacy `selectMove × 24` vs `selectMoveWithStateHint × 24 + executeBatch × 3` produces identical end state. + +**Phase 3 — Transpiler parity (deferred).** Local TS engine continues running single-turn `execute()` against hydrated state. Eventual batched parity desired but not v1. + +**Phase 4 — Optional cutover.** If `executeShadowed` (B=1) is gas-neutral or better, consider redirecting. Otherwise keep the legacy fast path. + +--- + +## 10. Test surface + +New `BattleHelper` helpers: +- `_submitTurnMoves(battleKey, turnId, p0Move, p1Move)` — synthesizes signatures and calls `submitTurnMoves`. +- `_executeBuffered(battleKey, numTurns)` — calls `executeBatch`. + +New tests: +- **Submission validation**: wrong revealer signer (parity), wrong turnId, wrong battleKey, replay, committer preimage hash mismatch. +- **Buffer ordering**: out-of-order rejected; batch executes in turnId order. +- **Switch-turn dispatch**: `flag == 0` and `flag == 1` ignore the non-acting half; non-acting player signing a non-NO_OP has no effect. +- **Equivalence (core gate)**: B turns through legacy path vs `submitTurnMoves × B + executeBatch` produce byte-identical state. +- **Game-over short-circuit** mid-batch. +- **Effect lifecycle parity**: BurnStatus DOT over a 4-turn batch matches per-turn execution. +- **Multi-batch in one battle**: two batches of 4, then one of 6 — `turnId` advances correctly. +- **Shadow flush**: post-batch `getMonStateForBattle` / `getGlobalKV` / `getEffects` match equivalent per-turn execution. +- **CPU equivalence**: 24-turn CPU game via legacy vs trusted-state batched produces identical end state. + +Existing tests stay untouched — they use the legacy entry points. + +Targeted equivalence tests for v1; differential fuzzing as a follow-up. diff --git a/snapshots/StandardAttackPvPGasTest.json b/snapshots/StandardAttackPvPGasTest.json index 020b58ba..ea0ef873 100644 --- a/snapshots/StandardAttackPvPGasTest.json +++ b/snapshots/StandardAttackPvPGasTest.json @@ -1,7 +1,7 @@ { - "Turn0_Lead": "64905", - "Turn1_BothAttack": "117165", - "Turn2_BothAttack": "77337", - "Turn3_BothAttack": "77398", - "Turn4_BothAttack": "77347" + "Turn0_Lead": "154149", + "Turn1_BothAttack": "218306", + "Turn2_BothAttack": "184102", + "Turn3_BothAttack": "190651", + "Turn4_BothAttack": "190600" } \ No newline at end of file diff --git a/test/BatchInstrumentationTest.sol b/test/BatchInstrumentationTest.sol new file mode 100644 index 00000000..6119dbf6 --- /dev/null +++ b/test/BatchInstrumentationTest.sol @@ -0,0 +1,352 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import "../lib/forge-std/src/Test.sol"; +import "../src/Constants.sol"; +import "../src/Enums.sol"; +import "../src/Structs.sol"; + +import {Engine} from "../src/Engine.sol"; +import {SignedCommitManager} from "../src/commit-manager/SignedCommitManager.sol"; +import {SignedCommitLib} from "../src/commit-manager/SignedCommitLib.sol"; +import {SignedMatchmaker} from "../src/matchmaker/SignedMatchmaker.sol"; +import {BattleOfferLib} from "../src/matchmaker/BattleOfferLib.sol"; +import {StandardAttackFactory} from "../src/moves/StandardAttackFactory.sol"; +import {ATTACK_PARAMS} from "../src/moves/StandardAttackStructs.sol"; +import {EIP712} from "../src/lib/EIP712.sol"; + +import {IEngine} from "../src/IEngine.sol"; +import {IEngineHook} from "../src/IEngineHook.sol"; +import {IEffect} from "../src/effects/IEffect.sol"; +import {IMoveSet} from "../src/moves/IMoveSet.sol"; +import {IRandomnessOracle} from "../src/rng/IRandomnessOracle.sol"; +import {IRuleset} from "../src/IRuleset.sol"; +import {IValidator} from "../src/IValidator.sol"; + +import {TypeCalculator} from "../src/types/TypeCalculator.sol"; +import {ITypeCalculator} from "../src/types/ITypeCalculator.sol"; + +import {TestTeamRegistry} from "./mocks/TestTeamRegistry.sol"; + +/// Counts SLOAD / SSTORE access patterns on a warm steady-state turn, to ground the PLAN_OPT.md +/// gas math in real data instead of estimates. +contract BatchInstrumentationTest is Test, EIP712 { + + uint256 constant MONS_PER_TEAM = 4; + uint256 constant MOVES_PER_MON = 4; + + uint256 constant P0_PK = 0xA11CE; + uint256 constant P1_PK = 0xB0B; + address p0; + address p1; + + Engine engine; + SignedCommitManager signedCommitManager; + SignedMatchmaker signedMatchmaker; + ITypeCalculator typeCalc; + TestTeamRegistry defaultRegistry; + StandardAttackFactory attackFactory; + + function _domainNameAndVersion() internal pure override returns (string memory, string memory) { + return ("SignedCommitManager", "1"); + } + + function setUp() public { + p0 = vm.addr(P0_PK); + p1 = vm.addr(P1_PK); + + engine = new Engine(MONS_PER_TEAM, MOVES_PER_MON, 1); + signedCommitManager = new SignedCommitManager(IEngine(address(engine))); + signedMatchmaker = new SignedMatchmaker(engine); + typeCalc = new TypeCalculator(); + defaultRegistry = new TestTeamRegistry(); + attackFactory = new StandardAttackFactory(typeCalc); + } + + function _startBattle(IRuleset ruleset) internal returns (bytes32) { + address[] memory makersToAdd = new address[](1); + makersToAdd[0] = address(signedMatchmaker); + address[] memory makersToRemove = new address[](0); + vm.prank(p0); + engine.updateMatchmakers(makersToAdd, makersToRemove); + vm.prank(p1); + engine.updateMatchmakers(makersToAdd, makersToRemove); + + (bytes32 battleKey, bytes32 pairHash) = engine.computeBattleKey(p0, p1); + uint256 nonce = engine.pairHashNonces(pairHash); + + BattleOffer memory offer = BattleOffer({ + battle: Battle({ + p0: p0, + p0TeamIndex: 0, + p1: p1, + p1TeamIndex: 0, + teamRegistry: defaultRegistry, + validator: IValidator(address(0)), + rngOracle: IRandomnessOracle(address(0)), + ruleset: ruleset, + moveManager: address(signedCommitManager), + matchmaker: signedMatchmaker, + engineHooks: new IEngineHook[](0) + }), + pairHashNonce: nonce + }); + + bytes32 structHash = BattleOfferLib.hashBattleOffer(offer); + bytes32 digest = signedMatchmaker.hashTypedData(structHash); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(P0_PK, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.prank(p1); + signedMatchmaker.startGame(offer, signature); + + return battleKey; + } + + function _signDualReveal( + uint256 privateKey, + bytes32 battleKey, + uint64 turnId, + bytes32 committerMoveHash, + uint8 revealerMoveIndex, + bytes32 revealerSalt, + uint240 revealerExtraData + ) internal view returns (bytes memory) { + bytes32 domainSeparator = keccak256( + abi.encode( + _DOMAIN_TYPEHASH, + keccak256("SignedCommitManager"), + keccak256("1"), + block.chainid, + address(signedCommitManager) + ) + ); + bytes32 structHash = SignedCommitLib.hashDualSignedReveal( + SignedCommitLib.DualSignedReveal({ + battleKey: battleKey, + turnId: turnId, + committerMoveHash: committerMoveHash, + revealerMoveIndex: revealerMoveIndex, + revealerSalt: revealerSalt, + revealerExtraData: revealerExtraData + }) + ); + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); + return abi.encodePacked(r, s, v); + } + + function _fastTurn( + bytes32 battleKey, + uint8 p0MoveIndex, + uint8 p1MoveIndex, + uint240 p0ExtraData, + uint240 p1ExtraData + ) internal { + uint64 turnId = uint64(engine.getTurnIdForBattleState(battleKey)); + bytes32 committerSalt = keccak256(abi.encode("committer", battleKey, turnId)); + bytes32 revealerSalt = keccak256(abi.encode("revealer", battleKey, turnId)); + + uint8 committerMoveIndex; + uint240 committerExtraData; + uint8 revealerMoveIndex; + uint240 revealerExtraData; + uint256 revealerPk; + address committer; + + if (turnId % 2 == 0) { + committerMoveIndex = p0MoveIndex; + committerExtraData = p0ExtraData; + revealerMoveIndex = p1MoveIndex; + revealerExtraData = p1ExtraData; + revealerPk = P1_PK; + committer = p0; + } else { + committerMoveIndex = p1MoveIndex; + committerExtraData = p1ExtraData; + revealerMoveIndex = p0MoveIndex; + revealerExtraData = p0ExtraData; + revealerPk = P0_PK; + committer = p1; + } + + bytes32 committerMoveHash = + keccak256(abi.encodePacked(committerMoveIndex, committerSalt, committerExtraData)); + bytes memory revealerSig = _signDualReveal( + revealerPk, battleKey, turnId, committerMoveHash, revealerMoveIndex, revealerSalt, revealerExtraData + ); + + vm.prank(committer); + signedCommitManager.executeWithDualSignedMoves( + battleKey, + committerMoveIndex, committerSalt, committerExtraData, + revealerMoveIndex, revealerSalt, revealerExtraData, + revealerSig + ); + engine.resetCallContext(); + } + + function _createMon(Type t1) internal pure returns (Mon memory) { + return Mon({ + stats: MonStats({ + hp: 10000, + stamina: 50, + speed: 10, + attack: 30, + defense: 10, + specialAttack: 30, + specialDefense: 10, + type1: t1, + type2: Type.None + }), + moves: new uint256[](0), + ability: 0 + }); + } + + /// @dev Iterates account accesses returned by stopAndReturnStateDiff and counts SLOAD/SSTORE + /// per (account, slot) — distinguishing first-touch (cold) from subsequent (warm), and + /// for SSTORE distinguishing zero→nonzero / nonzero→nonzero / no-op. + function _summarizeAccesses(Vm.AccountAccess[] memory accesses) + internal + pure + returns ( + uint256 totalSloadCount, + uint256 totalSstoreCount, + uint256 coldSloads, + uint256 warmSloads, + uint256 coldSstores, + uint256 warmSstores, + uint256 zeroToNonzeroSstores, + uint256 nonzeroToNonzeroSstores, + uint256 noopSstores, + uint256 uniqueSlotsTouched, + uint256 multiWriteSlots + ) + { + // Count slot-touch frequencies via a small fixed-capacity table (we don't expect many uniques) + bytes32[] memory keys = new bytes32[](256); + uint8[] memory writes = new uint8[](256); + bool[] memory reads = new bool[](256); + uint256 keyCount; + + for (uint256 i = 0; i < accesses.length; i++) { + Vm.StorageAccess[] memory storageAccesses = accesses[i].storageAccesses; + for (uint256 j = 0; j < storageAccesses.length; j++) { + Vm.StorageAccess memory a = storageAccesses[j]; + bytes32 key = keccak256(abi.encode(a.account, a.slot)); + + // Locate or create entry + uint256 idx = keyCount; + for (uint256 k = 0; k < keyCount; k++) { + if (keys[k] == key) { + idx = k; + break; + } + } + if (idx == keyCount) { + keys[idx] = key; + keyCount++; + } + + if (a.isWrite) { + totalSstoreCount++; + writes[idx]++; + if (a.previousValue == bytes32(0) && a.newValue != bytes32(0)) zeroToNonzeroSstores++; + else if (a.previousValue != bytes32(0) && a.newValue != bytes32(0) && a.previousValue != a.newValue) + nonzeroToNonzeroSstores++; + else if (a.previousValue == a.newValue) noopSstores++; + + if (writes[idx] == 1 && !reads[idx]) { + coldSstores++; + } else { + warmSstores++; + } + } else { + totalSloadCount++; + if (!reads[idx] && writes[idx] == 0) { + coldSloads++; + reads[idx] = true; + } else { + warmSloads++; + } + } + } + } + + uniqueSlotsTouched = keyCount; + for (uint256 i = 0; i < keyCount; i++) { + if (writes[i] >= 2) multiWriteSlots++; + } + } + + /// @notice Per-turn storage-access profile for a clean PvP damage-trade turn (steady state). + function test_storageAccessProfile_cleanDamageTradeTurn() public { + IMoveSet moveA = attackFactory.createAttack( + ATTACK_PARAMS({ + BASE_POWER: 30, STAMINA_COST: 1, ACCURACY: 100, PRIORITY: 1, + MOVE_TYPE: Type.Fire, EFFECT_ACCURACY: 0, MOVE_CLASS: MoveClass.Physical, + CRIT_RATE: 0, VOLATILITY: 0, NAME: "AttackA", EFFECT: IEffect(address(0)) + }) + ); + IMoveSet moveB = attackFactory.createAttack( + ATTACK_PARAMS({ + BASE_POWER: 25, STAMINA_COST: 1, ACCURACY: 100, PRIORITY: 1, + MOVE_TYPE: Type.Fire, EFFECT_ACCURACY: 0, MOVE_CLASS: MoveClass.Special, + CRIT_RATE: 0, VOLATILITY: 0, NAME: "AttackB", EFFECT: IEffect(address(0)) + }) + ); + + Mon memory mon = _createMon(Type.Fire); + mon.moves = new uint256[](MOVES_PER_MON); + mon.moves[0] = uint256(uint160(address(moveA))); + mon.moves[1] = uint256(uint160(address(moveB))); + mon.moves[2] = uint256(uint160(address(moveA))); + mon.moves[3] = uint256(uint160(address(moveB))); + + Mon[] memory team = new Mon[](MONS_PER_TEAM); + for (uint256 i; i < MONS_PER_TEAM; i++) team[i] = mon; + defaultRegistry.setTeam(p0, team); + defaultRegistry.setTeam(p1, team); + + IRuleset ruleset = IRuleset(INLINE_STAMINA_REGEN_RULESET); + bytes32 battleKey = _startBattle(ruleset); + vm.warp(vm.getBlockTimestamp() + 1); + + // Warm-up: lead-in switch + 1 damage trade. + _fastTurn(battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _fastTurn(battleKey, 0, 0, 0, 0); + + // Now profile a steady-state warm turn. + vm.startStateDiffRecording(); + _fastTurn(battleKey, 1, 1, 0, 0); + Vm.AccountAccess[] memory diffs = vm.stopAndReturnStateDiff(); + + ( + uint256 totalSload, + uint256 totalSstore, + uint256 coldSload, + uint256 warmSload, + uint256 coldSstore, + uint256 warmSstore, + uint256 z2nz, + uint256 nz2nz, + uint256 noop, + uint256 unique, + uint256 multiWrite + ) = _summarizeAccesses(diffs); + + console.log("=== CLEAN DAMAGE-TRADE TURN - STORAGE PROFILE ==="); + console.log("Total SLOADs :", totalSload); + console.log(" Cold (first-touch in tx) :", coldSload); + console.log(" Warm :", warmSload); + console.log("Total SSTOREs :", totalSstore); + console.log(" Cold (first-touch in tx) :", coldSstore); + console.log(" Warm :", warmSstore); + console.log(" zero -> nonzero :", z2nz); + console.log(" nonzero -> nonzero (diff) :", nz2nz); + console.log(" no-op (same value) :", noop); + console.log("Unique slots touched :", unique); + console.log("Slots written 2+ times in turn :", multiWrite); + } +} From 21aaee3ab669f9c66b76f7dd3d394a36abeba10f Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Mon, 27 Apr 2026 16:25:37 -0700 Subject: [PATCH 02/10] wip --- OPT_PLAN.md | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/OPT_PLAN.md b/OPT_PLAN.md index 1d6a56c3..acd852e3 100644 --- a/OPT_PLAN.md +++ b/OPT_PLAN.md @@ -15,11 +15,13 @@ Secondary goal: route `Engine` state access through helpers so the single-turn p ### 2.1 Per-turn submission (PvP) `SignedCommitManager.submitTurnMoves(battleKey, TurnSubmission entry)`: -- Uniform shape every turn: one revealer EIP-712 signature, committer preimage in calldata. Roles derived from `turnId % 2` (matching `getCommitAuthForDualSigned`). +- Uniform shape every turn: **two EIP-712 signatures** (committer + revealer), committer preimage in calldata. Roles derived from `turnId % 2` (matching `getCommitAuthForDualSigned`). - Switch turns use the same shape. The non-acting player signs a `NO_OP` (move 126); engine ignores their half at batch time using the live `playerSwitchForTurnFlag`. -- Manager hashes committer preimage, verifies revealer sig over `DualSignedReveal`, writes to `moveBuffer[storageKey][turnId]`. **No execute runs.** +- Manager hashes committer preimage, verifies committer sig over `SignedCommit{committerMoveHash, …}` and revealer sig over `DualSignedReveal{committerMoveHash, …}`, writes to `moveBuffer[storageKey][turnId]`. **No execute runs.** - Updates `lastSubmitTimestamp` for timeout tracking. +**Why two sigs.** Without a committer sig, a malicious revealer could pick any preimage `P*`, sign `DualSignedReveal{committerMoveHash: keccak(P*), …}`, and submit unilaterally — the contract would play `P*` as the committer's move with no committer involvement. Today's `executeWithDualSignedMoves` blocks this only via `msg.sender == committer`, which is fragile and not relayer-friendly. Phase 0 (§9) lifts the same fix into the existing function before any batching ships, so both paths share one security model. + ### 2.2 Per-batch execute `Engine.executeBatch(battleKey, numTurns)`: @@ -75,7 +77,16 @@ struct TurnSubmission { uint8 revealerMoveIndex; uint16 revealerExtraData; uint104 revealerSalt; - bytes sig; // revealer EIP-712 over DualSignedReveal + // Sigs: + bytes committerSig; // EIP-712 over SignedCommit{committerMoveHash, battleKey, turnId} + bytes revealerSig; // EIP-712 over DualSignedReveal +} + +// Existing SignedCommitLib struct, reused unchanged. +struct SignedCommit { + bytes32 moveHash; + bytes32 battleKey; + uint64 turnId; } struct DualSignedReveal { @@ -95,8 +106,9 @@ Manager flow: 2. `entry.turnId` equals next append position. 3. Derive `(committer, revealer)` from `turnId % 2`. 4. `committerMoveHash = keccak(committerMoveIndex, committerSalt, committerExtraData)`. -5. Recover signer over `DualSignedReveal`; require equality with `revealer`. -6. Map fields to `(p0, p1)` by parity; SSTORE `PackedTurnEntry`. +5. Recover `committerSig` over `SignedCommit{committerMoveHash, battleKey, turnId}`; require equality with `committer`. +6. Recover `revealerSig` over `DualSignedReveal{committerMoveHash, …}`; require equality with `revealer`. +7. Map fields to `(p0, p1)` by parity; SSTORE `PackedTurnEntry`. ### 4.2 Batch execute @@ -275,7 +287,19 @@ Validator/legality is unchanged: signature recovery proves player intent (or `ms ## 9. Phased rollout -**Phase 0 — Instrumentation refresh.** `test/BatchInstrumentationTest.sol` already wires `vm.startStateDiffRecording` for the clean damage-trade case. Add scenarios: effect-heavy turn (status DOT + StatBoosts active), forced-switch turn, multi-mon turn. Lock final batch-size guidance. +**Phase 0 — Dual-sig security fix (preflight, ships first, independent of batching).** The existing `executeWithDualSignedMoves` relies on `msg.sender == committer` as the committer's binding. Without that check, a malicious revealer could sign `DualSignedReveal{committerMoveHash: keccak(P*), …}` for any preimage `P*` they choose and submit unilaterally — the contract would happily compute `committerMoveHash = keccak(P*)`, recover the revealer's sig, and play `P*` as the committer's move. The check is load-bearing today, but it's also fragile: any future evolution of the flow that drops or weakens it (relayers, batching, alt entry points) silently re-opens the hole. + +Fix: require an explicit committer signature over the existing `SignedCommit{moveHash, battleKey, turnId}` struct (already used by `commitWithSignature`). + +- Modify `executeWithDualSignedMoves` to take an additional `bytes calldata committerSignature` parameter. +- Recover `committerSignature` over `SignedCommit{committerMoveHash, battleKey, turnId}`; require equality with `committer`. +- Drop the `msg.sender == committer` check; the function becomes relayer-friendly (anyone with both sigs + the preimage can submit). +- Breaking signature change. Update all callers (tests, `BattleHelper`, anything off-chain that calls this function) in the same PR. No deployed callers in production yet. +- New tests: missing committer sig reverts; wrong committer signer reverts; submission by a third party with both valid sigs succeeds; revealer cannot submit a self-chosen committer preimage (regression). + +This phase ships before any batching work. It hardens the existing flow on its own merits and unifies the security model so the batched path in Phase 2 inherits the same shape (§4.1) without surprises. + +**Phase 0.1 — Instrumentation refresh.** `test/BatchInstrumentationTest.sol` already wires `vm.startStateDiffRecording` for the clean damage-trade case. Add scenarios: effect-heavy turn (status DOT + StatBoosts active), forced-switch turn, multi-mon turn. Lock final batch-size guidance. **Phase 0.5 — Helper extraction (no behavior change).** Replace direct `MonState`/`globalKV`/effect-data SLOAD/SSTORE in `Engine.sol` with §5.2 helpers, with `_shadowActive` permanently `false`. Snapshot diff should be roughly flat. @@ -298,7 +322,7 @@ New `BattleHelper` helpers: - `_executeBuffered(battleKey, numTurns)` — calls `executeBatch`. New tests: -- **Submission validation**: wrong revealer signer (parity), wrong turnId, wrong battleKey, replay, committer preimage hash mismatch. +- **Submission validation**: wrong committer signer, wrong revealer signer (parity), wrong turnId, wrong battleKey, replay, committer preimage hash mismatch, missing committer sig (regression for unilateral-revealer attack), missing revealer sig. - **Buffer ordering**: out-of-order rejected; batch executes in turnId order. - **Switch-turn dispatch**: `flag == 0` and `flag == 1` ignore the non-acting half; non-acting player signing a non-NO_OP has no effect. - **Equivalence (core gate)**: B turns through legacy path vs `submitTurnMoves × B + executeBatch` produce byte-identical state. From a0af23814c62fcb6d300c7bbb35a262fad9961d9 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Mon, 27 Apr 2026 21:25:30 -0700 Subject: [PATCH 03/10] wip --- OPT_PLAN.md | 71 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/OPT_PLAN.md b/OPT_PLAN.md index acd852e3..73f542ff 100644 --- a/OPT_PLAN.md +++ b/OPT_PLAN.md @@ -4,7 +4,7 @@ Amortize per-turn cold-storage access in `Engine.execute()` by: 1. Submitting each turn's signed moves on-chain immediately to a per-turn buffer (no execute). -2. Executing `N` buffered turns in one tx with engine state held in **transient shadow storage**, flushed to persistent storage once at the end. +2. Executing **all currently buffered turns** in one tx with engine state held in **transient shadow storage**, flushed to persistent storage once at the end. Secondary goal: route `Engine` state access through helpers so the single-turn path can also use the shadow layer. @@ -24,10 +24,11 @@ Secondary goal: route `Engine` state access through helpers so the single-turn p ### 2.2 Per-batch execute -`Engine.executeBatch(battleKey, numTurns)`: +`Engine.executeBatch(battleKey)`: - Anyone can call (sigs were checked at submission). -- Reads buffered entries `[BattleData.turnId, BattleData.turnId + numTurns)`, runs each in sequence inside transient shadow storage, flushes once at end. -- `BattleData.turnId` advances inside the loop; next batch starts at the right slot. +- Reads every currently buffered entry `[startTurn, startTurn + numTurnsBuffered)`, runs each in sequence inside transient shadow storage, flushes once at end. +- The **transient mirror** of `turnId` advances inside the loop. Persistent `BattleData.turnId` advances only during the final flush. +- Batch execution always consumes the full pending buffer. There is no partial-batch mode in v1. - Processed buffer slots are not cleared — the unbounded mapping leaves them for on-chain replay. Slot reuse across battles comes from `MappingAllocator`. ### 2.3 Fallback / stalls @@ -56,6 +57,22 @@ mapping(bytes32 storageKey => mapping(uint64 turnId => PackedTurnEntry)) moveBuf Steady-state cost per turn: 1 SSTORE (5k, nonzero→nonzero from prior battle's slot reuse) + 1 SLOAD inside batch (2.1k) = ~7.1k. +Buffer validity is tracked by two packed `uint8` counters: +- `numTurnsBuffered`: number of currently pending buffered turns. +- `numTurnsExecuted`: cumulative number of buffered turns consumed for the current battle/storage key. + +Submit rule: +- If `numTurnsBuffered == 0`, the manager first syncs `numTurnsExecuted` to the engine's current `BattleData.turnId`. This keeps the batched buffer compatible with legacy single-turn execution when the battle alternates modes. +- A new entry must have `entry.turnId == numTurnsExecuted + numTurnsBuffered`. +- After storing the entry, increment `numTurnsBuffered`. + +Execute rule: +- `executeBatch` requires `numTurnsBuffered > 0`. +- It attempts the full pending range of `numTurnsBuffered` turns, starting at `numTurnsExecuted`. +- At flush, persistent `BattleData.turnId` becomes the shadowed turn id, `numTurnsExecuted += executedTurns`, and `numTurnsBuffered = 0`. + +This means stale slots from a prior battle or earlier batch cannot be treated as valid pending moves: only the contiguous range described by `(numTurnsExecuted, numTurnsBuffered)` is live. + **Width changes (clean break):** - `extraData`: 240 → 16 bits. Audit confirmed all production consumers read ≤8 bits. Narrow `IMoveSet.move()`'s `extraData` param to `uint16`; repack test helpers (`_packStatBoost`, `StatBoostsMove` mock). - `Salt`: 256 → 104 bits. 2^104 brute-force resistance is sufficient for the seconds-to-minutes commit-reveal window. @@ -113,13 +130,14 @@ Manager flow: ### 4.2 Batch execute ```solidity -function executeBatch(bytes32 battleKey, uint64 numTurns) external; +function executeBatch(bytes32 battleKey) external; ``` -1. Read `startTurn = BattleData.turnId`; require turns `[startTurn, startTurn+numTurns)` all buffered. +1. Read `startTurn = numTurnsExecuted`; require `numTurnsBuffered > 0`. 2. Hydrate shadow. -3. For each turn: read buffer slot, populate per-turn move/salt transient, run `_executeOneTurn()`, break on game-over. +3. For each pending buffered turn: read buffer slot, populate per-turn move/salt transient, run `_executeOneTurn()`, break on game-over. 4. Flush shadow → storage. +5. Set `numTurnsBuffered = 0` and increment `numTurnsExecuted` by the number of turns actually executed. --- @@ -132,14 +150,14 @@ function executeBatch(bytes32 battleKey, uint64 numTurns) external; | `MonState` (per mon) | Per-`(playerIndex, monIndex)` mirror, lazy-loaded. Dirty bit per slot. | | `koBitmaps` (16 bits in `BattleConfig` slot 2) | `uint16` mirror, loaded flag. | | `winnerIndex` / `prevPlayerSwitchForTurnFlag` / `playerSwitchForTurnFlag` / `activeMonIndex` / `turnId` / `lastExecuteTimestamp` | Single packed `uint256` mirror. | -| Effect data slots (`globalEffects[i].data`, `pXEffects[i].data`) | Sparse transient map keyed by slot index, mirrors `data` only. `effect`/`stepsBitmap` read from storage (warm after first hit). | +| Effect list slots (`globalEffects[i]`, `pXEffects[i]`) | Sparse transient map keyed by list + slot index, mirrors the full `EffectInstance` (`effect`, `stepsBitmap`, `data`). | | `packedP0EffectsCount` / `packedP1EffectsCount` / `globalEffectsLength` | Three small mirrors, flushed with effect-list shadow. | | `globalKV[storageKey][key]` | Per-`key` mirror, lazy-loaded. | | `BattleConfig.p0Move` / `p1Move` / salts | Re-populated per sub-turn from buffer slot. | Hydrate strategy: - **Eager**: `BattleData` slot 1 + `BattleConfig` slot 2 (always touched). -- **Lazy**: `MonState`, effect data, `globalKV` (sparse — pay only for slots touched). +- **Lazy**: `MonState`, effect slots/counts, `globalKV` (sparse — pay only for slots touched). Loaded-flag strategy: - **Bitmap** for fixed-shape slots (MonState, effects, slot-2 packed fields). @@ -154,18 +172,27 @@ function _shadowReadMonState(BattleConfig storage cfg, uint256 playerIndex, uint function _shadowWriteMonState(uint256 playerIndex, uint256 monIndex, MonState memory state) internal; function _shadowReadKV(bytes32 storageKey, uint64 key) internal returns (uint192); function _shadowWriteKV(bytes32 storageKey, uint64 key, uint192 value) internal; -function _shadowReadEffectData(uint256 effectList, uint256 monIndex, uint256 slotIndex) internal returns (bytes32); -function _shadowWriteEffectData(uint256 effectList, uint256 monIndex, uint256 slotIndex, bytes32 data) internal; +function _shadowReadEffectSlot(uint256 effectList, uint256 monIndex, uint256 slotIndex) internal returns (EffectInstance memory); +function _shadowWriteEffectSlot(uint256 effectList, uint256 monIndex, uint256 slotIndex, EffectInstance memory eff) internal; +function _shadowReadEffectCount(uint256 effectList, uint256 monIndex) internal returns (uint256); +function _shadowWriteEffectCount(uint256 effectList, uint256 monIndex, uint256 count) internal; ``` When `_shadowActive == false`, helpers SLOAD/SSTORE storage directly. When `true`, they read/write the transient mirror with lazy-load and dirty-bit bookkeeping. External `IEngine` writers (`updateMonState`, `dealDamage`, `addEffect`, `removeEffect`, `editEffect`, `setGlobalKV`, `switchActiveMon`, `dispatchStandardAttack`, `setMove`) and external readers (`getMonStateForBattle`, `getEffects`, `getGlobalKV`, etc.) all route through these helpers. The `battleKeyForWrite != bytes32(0)` gate stays. +Effect-list shadowing must preserve these same-batch visibility rules: +- `addEffect` writes a full shadow `EffectInstance` and increments the shadow count, so later effect loops / `getEffects` calls in the same batch see the new effect. +- `editEffect` updates shadow `data`; later hooks see the edited value. +- `removeEffect` tombstones the shadow `effect` address and keeps the slot index stable; later loops skip it. +- `_handleEffects` loads counts and slots from shadow, not storage, and keeps the existing `effectsDirtyBitmap` pattern so effects added while iterating can extend the current loop when today’s logic would. +- `getEffects` builds its return arrays from shadow while `_shadowActive == true`, so external moves/effects that inspect active effects observe the live batch state. + ### 5.3 Batch loop ``` -executeBatch(battleKey, numTurns): +executeBatch(battleKey): storageKey = _getStorageKey(battleKey) storageKeyForWrite = storageKey battleKeyForWrite = battleKey @@ -174,8 +201,9 @@ executeBatch(battleKey, numTurns): _hydrateBattleData(battleKey) _hydrateConfigSlot2(storageKey) - startTurn = BattleData.turnId - for t in [startTurn .. startTurn + numTurns): + startTurn = numTurnsExecuted + turnsToExecute = numTurnsBuffered + for t in [startTurn .. startTurn + turnsToExecute): bufferEntry = _readMoveBufferSlot(storageKey, t) _populateTurnMoveTransient(bufferEntry) _executeOneTurn() @@ -185,8 +213,9 @@ executeBatch(battleKey, numTurns): _flushBattleData(battleKey) _flushConfigSlot2(storageKey) _flushDirtyMonStates(storageKey) - _flushDirtyEffectData(storageKey) + _flushDirtyEffectSlots(storageKey) _flushDirtyGlobalKV(storageKey) + _flushBufferCounters(executedTurns) _shadowActive = false ``` @@ -210,7 +239,7 @@ Submission validates only cheap invariants (battle exists, not over at last flus ### 6.2 Game-over mid-batch -`_executeInternal` already breaks when `winnerIndex != 2`. Same check stops the batch loop. Remaining buffered entries stay untouched. +`_executeInternal` already breaks when `winnerIndex != 2`. Same check stops the batch loop. Because batch execution consumes the full pending buffer, any unexecuted buffered entries after game-over remain in storage for replay but are no longer live; `numTurnsBuffered` is set to zero at flush. ### 6.3 Status-induced skip-turn @@ -250,7 +279,7 @@ function selectMoveWithStateHint( ) external; ``` -1. Read current `turnId` from `BattleData`. +1. Read/sync the next append `turnId` from `numTurnsExecuted + numTurnsBuffered` using the same buffer counter rules as PvP. 2. Require `msg.sender == alice`. 3. Route on `projectedState.playerSwitchForTurnFlag` (single-player vs two-player CPU branch). 4. `ICPU(cpuAddr).calculateMove(projectedState, aliceMoveIndex, aliceExtraData)` → `(cpuMove, cpuExtra)`. CPU reads from calldata only. @@ -305,7 +334,7 @@ This phase ships before any batching work. It hardens the existing flow on its o **Phase 1 — Single-turn shadow.** Implement transient mirrors + lazy-load/dirty-flag bookkeeping. Wire helpers to consult `_shadowActive`. Add `executeShadowed(bytes32 battleKey)` that does `execute()`'s work inside the shadow layer (hydrate → run one turn → flush). Existing test suite should pass against it. B=1 will be slightly *worse* than today's `execute()` due to bookkeeping overhead; expected. -**Phase 2 — PvP per-turn submission + batch execute.** Extend `SignedCommitManager` with `submitTurnMoves`. Add per-turn move buffer mapping. Add `Engine.executeBatch` with flag-based dispatch (§6.1). Equivalence tests + gas snapshots. +**Phase 2 — PvP per-turn submission + batch execute.** Extend `SignedCommitManager` with `submitTurnMoves`. Add per-turn move buffer mapping and `numTurnsBuffered` / `numTurnsExecuted` counters. Add `Engine.executeBatch` with flag-based dispatch (§6.1), requiring execution of all currently buffered turns. Equivalence tests + gas snapshots. **Phase 2.5 — CPU mode.** Extend `CPUMoveManager` with `selectMoveWithStateHint` (§7.4). Reuse Phase-2 buffer + `executeBatch`. Equivalence test: 24-turn CPU game via legacy `selectMove × 24` vs `selectMoveWithStateHint × 24 + executeBatch × 3` produces identical end state. @@ -319,16 +348,16 @@ This phase ships before any batching work. It hardens the existing flow on its o New `BattleHelper` helpers: - `_submitTurnMoves(battleKey, turnId, p0Move, p1Move)` — synthesizes signatures and calls `submitTurnMoves`. -- `_executeBuffered(battleKey, numTurns)` — calls `executeBatch`. +- `_executeBuffered(battleKey)` — calls `executeBatch` for all currently buffered turns. New tests: - **Submission validation**: wrong committer signer, wrong revealer signer (parity), wrong turnId, wrong battleKey, replay, committer preimage hash mismatch, missing committer sig (regression for unilateral-revealer attack), missing revealer sig. - **Buffer ordering**: out-of-order rejected; batch executes in turnId order. - **Switch-turn dispatch**: `flag == 0` and `flag == 1` ignore the non-acting half; non-acting player signing a non-NO_OP has no effect. - **Equivalence (core gate)**: B turns through legacy path vs `submitTurnMoves × B + executeBatch` produce byte-identical state. -- **Game-over short-circuit** mid-batch. +- **Game-over short-circuit** mid-batch: remaining stored buffer entries are no longer live after `numTurnsBuffered` resets to zero. - **Effect lifecycle parity**: BurnStatus DOT over a 4-turn batch matches per-turn execution. -- **Multi-batch in one battle**: two batches of 4, then one of 6 — `turnId` advances correctly. +- **Multi-batch in one battle**: submit 4 then execute, submit 4 then execute, submit 6 then execute — `turnId`, `numTurnsBuffered`, and `numTurnsExecuted` advance correctly. - **Shadow flush**: post-batch `getMonStateForBattle` / `getGlobalKV` / `getEffects` match equivalent per-turn execution. - **CPU equivalence**: 24-turn CPU game via legacy vs trusted-state batched produces identical end state. From 50a7f24c40d165fec56e4b602749fb029495835f Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Tue, 28 Apr 2026 09:58:34 -0700 Subject: [PATCH 04/10] wip transpiler fixes --- OPT_PLAN.md | 108 +++++++++++++++++++++++- transpiler/README.md | 36 +++++--- transpiler/__init__.py | 2 +- transpiler/codegen/expression.py | 45 +++------- transpiler/codegen/imports.py | 28 ++---- transpiler/codegen/statement.py | 14 +++ transpiler/docs/extending.md | 2 +- transpiler/docs/init.md | 5 +- transpiler/docs/quickstart.md | 21 +++-- transpiler/docs/runtime-replacements.md | 38 +++++---- transpiler/docs/runtime.md | 6 +- transpiler/docs/semantics.md | 23 ++--- transpiler/sol2ts.py | 49 +++++++---- transpiler/transpiler-config.json | 2 +- 14 files changed, 259 insertions(+), 120 deletions(-) diff --git a/OPT_PLAN.md b/OPT_PLAN.md index 73f542ff..ad353844 100644 --- a/OPT_PLAN.md +++ b/OPT_PLAN.md @@ -150,7 +150,7 @@ function executeBatch(bytes32 battleKey) external; | `MonState` (per mon) | Per-`(playerIndex, monIndex)` mirror, lazy-loaded. Dirty bit per slot. | | `koBitmaps` (16 bits in `BattleConfig` slot 2) | `uint16` mirror, loaded flag. | | `winnerIndex` / `prevPlayerSwitchForTurnFlag` / `playerSwitchForTurnFlag` / `activeMonIndex` / `turnId` / `lastExecuteTimestamp` | Single packed `uint256` mirror. | -| Effect list slots (`globalEffects[i]`, `pXEffects[i]`) | Sparse transient map keyed by list + slot index, mirrors the full `EffectInstance` (`effect`, `stepsBitmap`, `data`). | +| Effect list slots (`globalEffects[i]`, `pXEffects[i]`) | Fixed numeric transient keys, mirrors the full `EffectInstance` (`effect`, `stepsBitmap`, `data`). | | `packedP0EffectsCount` / `packedP1EffectsCount` / `globalEffectsLength` | Three small mirrors, flushed with effect-list shadow. | | `globalKV[storageKey][key]` | Per-`key` mirror, lazy-loaded. | | `BattleConfig.p0Move` / `p1Move` / salts | Re-populated per sub-turn from buffer slot. | @@ -163,6 +163,79 @@ Loaded-flag strategy: - **Bitmap** for fixed-shape slots (MonState, effects, slot-2 packed fields). - **Per-key transient hash-set** for `globalKV` (dynamic keys). +### 5.1.1 Effect shadow key layout + +Effects are bounded and already partitioned, so use numeric transient keys and bitmaps instead of hashed keys. + +Assumptions: +- Up to 8 mons per side. +- Up to 8 effects per mon. +- Up to 16 global effects. + +Flat effect-slot keys: + +```solidity +uint256 constant EFFECTS_PER_MON = 8; +uint256 constant MONS_PER_SIDE = 8; +uint256 constant MAX_GLOBAL_EFFECTS = 16; + +uint256 constant EFFECT_P0_OFFSET = 0; // keys 0..63 +uint256 constant EFFECT_P1_OFFSET = 64; // keys 64..127 +uint256 constant EFFECT_GLOBAL_OFFSET = 128; // keys 128..143 + +function _effectShadowKey(uint256 targetIndex, uint256 monIndex, uint256 localEffectIndex) + internal + pure + returns (uint256) +{ + if (targetIndex == 2) return EFFECT_GLOBAL_OFFSET + localEffectIndex; + uint256 sideOffset = targetIndex == 0 ? EFFECT_P0_OFFSET : EFFECT_P1_OFFSET; + return sideOffset + monIndex * EFFECTS_PER_MON + localEffectIndex; +} +``` + +For player effects, `localEffectIndex` is `0..7` and the storage slot remains +`_getEffectSlotIndex(monIndex, localEffectIndex)`. For global effects, `monIndex` is ignored and +`localEffectIndex` is the global effect index. + +Loaded/dirty bitmaps: + +```solidity +uint256 transient effectSlotLoadedBitmap; +uint256 transient effectSlotDirtyBitmap; + +function _effectBit(uint256 key) internal pure returns (uint256) { + return 1 << key; +} +``` + +Shadow values can use numeric transient key regions, one region per `EffectInstance` field: + +```solidity +uint256 constant T_EFFECT_ADDR_BASE = 0x1000; +uint256 constant T_EFFECT_STEPS_BASE = 0x2000; +uint256 constant T_EFFECT_DATA_BASE = 0x3000; + +// tstore(T_EFFECT_ADDR_BASE + key, address(effect)) +// tstore(T_EFFECT_STEPS_BASE + key, stepsBitmap) +// tstore(T_EFFECT_DATA_BASE + key, data) +``` + +Counts use a separate compact key space: + +```solidity +// 0 = globalEffectsLength +// 1..8 = p0 mon counts +// 9..16 = p1 mon counts +function _effectCountKey(uint256 targetIndex, uint256 monIndex) internal pure returns (uint256) { + if (targetIndex == 2) return 0; + if (targetIndex == 0) return 1 + monIndex; + return 9 + monIndex; +} +``` + +Use separate loaded/dirty bitmaps for counts. Flush scans only dirty effect-slot bits in `0..143` and dirty count bits in `0..16`, so flush work is bounded and independent of calldata shape. + ### 5.2 Helper boundary Mirrored helpers in `Engine.sol`: @@ -364,3 +437,36 @@ New tests: Existing tests stay untouched — they use the legacy entry points. Targeted equivalence tests for v1; differential fuzzing as a follow-up. + +### 10.1 Effect-shadow correctness tests + +Correctness target: for any scripted turn sequence, batched execution produces the same final battle state and the same mid-execution observations as legacy single-turn execution would produce after each turn. + +Use a small purpose-built mock effect/move suite instead of relying only on production mons: + +- `AddEffectOnRun`: during a hook, calls `engine.addEffect` to append another effect to the same list. +- `EditSelfOnRun`: calls `engine.editEffect` on its own slot and increments a counter in `data`. +- `RemoveSelfOnRun`: returns `removeAfterRun = true`. +- `RemoveOtherOnRun`: calls `engine.removeEffect` for another slot. +- `InspectEffectsOnRun`: calls `engine.getEffects` during the batch and records/validates the visible list. +- `SingletonAbilityRegister`: exercises ability-triggered self-registration through `_activateAbility`. + +Required cases: + +- **Add visibility:** an effect added on sub-turn `T` is visible to `getEffects` and to `_handleEffects` on sub-turn `T+1`. +- **Add during iteration:** when an effect adds another effect while `_handleEffects` is iterating, the shadow count + `effectsDirtyBitmap` behavior matches legacy storage behavior. +- **Edit visibility:** data written by `editEffect` or returned from a hook is visible to later hooks in the same batch. +- **Remove visibility:** a removed effect is tombstoned in shadow, skipped by later `_handleEffects`, and omitted from `getEffects`, with slot indices preserved. +- **OnRemove callback:** removing an effect with `OnRemove` sees shadowed active mon indices and can perform shadowed writes. +- **Singleton/idempotency:** ability self-registration checks the shadow list, so repeated activation in one batch does not duplicate an effect. +- **Global effects:** repeat add/edit/remove/getEffects cases for global effects, including index `15` to cover the `MAX_GLOBAL_EFFECTS = 16` boundary. +- **Per-player boundaries:** cover p0 mon 0, p0 mon 7, p1 mon 0, and p1 mon 7 to exercise numeric key offsets. +- **Capacity:** adding a ninth effect to one mon or a seventeenth global effect fails/no-ops according to the chosen production behavior, and never corrupts adjacent shadow keys. +- **Flush parity:** after batch flush, storage `EffectInstance` slots and counts match the legacy run byte-for-byte, including tombstones. + +Test shape: + +1. Start two identical battles. +2. Run the same scripted turns through legacy single-turn execution in battle A. +3. Submit all turns, execute one full batch in battle B. +4. Compare `BattleData`, mon states, `globalKV`, `getEffects` for all relevant lists, and any mock-recorded observations. diff --git a/transpiler/README.md b/transpiler/README.md index f19811c4..9656b553 100644 --- a/transpiler/README.md +++ b/transpiler/README.md @@ -24,25 +24,39 @@ into TypeScript you can run in Node or the browser. ```bash git clone ~/tools/extruder -cd ~/tools/extruder && pip install -r requirements.txt +cd ~/tools/extruder # In your own Foundry project: npm install -D viem vitest ``` +Run extruder as a Python module from the directory that contains the +`transpiler/` package: + +```bash +cd ~/tools/extruder +python3 -m transpiler --help +``` + +Do not run `transpiler/sol2ts.py` directly; it uses package-relative imports. + ## Quickstart Bootstrap the config and scaffolded runtime-replacement stubs with one command: ```bash -python3 ~/tools/extruder/sol2ts.py init src/ --yes +cd ~/tools/extruder +python3 -m transpiler init /path/to/your/foundry/project/src --yes ``` Then transpile: ```bash -python3 ~/tools/extruder/sol2ts.py src/ -o ts-output -d src --emit-metadata +python3 -m transpiler /path/to/your/foundry/project/src \ + -o /path/to/your/foundry/project/ts-output \ + -d /path/to/your/foundry/project/src \ + --emit-metadata ``` See [`docs/quickstart.md`](docs/quickstart.md) for the full walkthrough. @@ -51,7 +65,7 @@ See [`docs/quickstart.md`](docs/quickstart.md) for the full walkthrough. - **Parse → AST → emit TS.** Not bytecode, not an EVM. - `uint*` / `int*` → `bigint`. `address` / `bytes*` → `string`. Mappings → - `Record`. Structs → interfaces + factory. Enums → `as const` objects. + `Record`. Structs → interfaces + factory. Enums → TypeScript `enum`s. - Contracts become ES classes extending a runtime `Contract` base that carries `_contractAddress`, `_storage`, `_msg`, an event emitter, and a transient-storage reset hook. @@ -70,9 +84,9 @@ counterpart. ## CLI ``` -extruder [input] [options] -extruder init [--yes] [--stub-output-dir DIR] [--config-path PATH] -extruder --emit-replacement-stub CONTRACT SOL_FILE [-o OUTPUT] +python3 -m transpiler [input] [options] +python3 -m transpiler init [--yes] [--stub-output-dir DIR] [--config-path PATH] +python3 -m transpiler --emit-replacement-stub CONTRACT SOL_FILE [-o OUTPUT] ``` | Flag | Purpose | @@ -82,8 +96,7 @@ extruder --emit-replacement-stub CONTRACT SOL_FILE [-o OUTPUT] | `-d`, `--discover` *(repeatable)* | Root(s) to scan for type discovery. Pass every source root you need cross-file resolution across. | | `--stdout` | Print a single file to stdout instead of writing (debugging). | | `--emit-metadata` | Also emit `factories.ts`. | -| `--metadata-only` | Skip TS generation, only write `factories.ts`. | -| `--overrides` | Path to `transpiler-config.json`. Defaults to the one next to `sol2ts.py`. | +| `--overrides` | Path to `transpiler-config.json`. Defaults to the one bundled with the package. | | `--emit-replacement-stub CONTRACT SOL_FILE` | Emit a TypeScript scaffold for a runtime replacement. Body = `throw new Error('Not implemented')`. See [`docs/runtime-replacements.md`](docs/runtime-replacements.md). | | `init ` | Scan a tree and scaffold a starter `transpiler-config.json` + runtime-replacement stubs. See [`docs/init.md`](docs/init.md). | @@ -103,12 +116,11 @@ extruder --emit-replacement-stub CONTRACT SOL_FILE [-o OUTPUT] reading before shipping. - [Extending](docs/extending.md) — contributor-facing; closing gaps in the transpiler itself. -- [FAQ](docs/faq.md) — currently empty; grows as real questions come in. ## Testing ```bash -python3 transpiler/test_transpiler.py +python3 -m transpiler.test_transpiler ``` Python unit tests cover the lexer, parser, codegen (Yul, type casts, @@ -117,4 +129,4 @@ resolver, and the `init` scan. ## License -AGPL-3.0. \ No newline at end of file +AGPL-3.0. diff --git a/transpiler/__init__.py b/transpiler/__init__.py index fb121c27..42975335 100644 --- a/transpiler/__init__.py +++ b/transpiler/__init__.py @@ -9,7 +9,7 @@ - parser/: AST nodes and parsing (Parser, all AST node types) - types/: Type registry and mappings (TypeRegistry, type conversion utilities) - codegen/: Code generation (TypeScriptCodeGenerator + specialized generators) -- sol2ts.py: Main transpiler entry point +- sol2ts.py: Main transpiler entry point (run with `python3 -m transpiler`) Usage: from transpiler import SolidityToTypeScriptTranspiler diff --git a/transpiler/codegen/expression.py b/transpiler/codegen/expression.py index 35e92a87..a26da907 100644 --- a/transpiler/codegen/expression.py +++ b/transpiler/codegen/expression.py @@ -30,6 +30,7 @@ TupleExpression, ArrayLiteral, TypeCast, + TypeName, ) @@ -546,38 +547,11 @@ def _handle_special_function(self, call: FunctionCall, name: str, args: str) -> def _handle_type_cast_call(self, call: FunctionCall, name: str, args: str) -> Optional[str]: """Handle type cast function calls (uint256(x), address(x), etc.).""" - if name.startswith('uint') or name.startswith('int'): - if call.arguments and isinstance(call.arguments[0], Identifier): + if self._is_primitive_cast_name(name): + if len(call.arguments) != 1: return args - if args.isdigit(): - return f'{args}n' - return self._type_converter._ensure_bigint(args) - elif name == 'address': - if call.arguments: - arg = call.arguments[0] - if isinstance(arg, Literal) and arg.kind in ('number', 'hex'): - return self._to_padded_address(arg.value) - if isinstance(arg, Identifier) and arg.name == 'this': - return 'this._contractAddress' - if self._is_already_address_type(arg): - return self.generate(arg) - if self._is_numeric_type_cast(arg): - inner = self.generate(arg) - return f'`0x${{({inner}).toString(16).padStart(40, "0")}}`' - inner = self.generate(arg) - if inner != 'this' and not inner.startswith('"') and not inner.startswith("'"): - return f'{inner}._contractAddress' - return args - elif name == 'bool': - return args - elif name == 'bytes32': - if call.arguments: - arg = call.arguments[0] - if isinstance(arg, Literal) and arg.kind in ('number', 'hex'): - return self._to_padded_bytes32(arg.value) - return args - elif name.startswith('bytes'): - return args + cast = TypeCast(type_name=TypeName(name=name), expression=call.arguments[0]) + return self._type_converter.generate_type_cast(cast, self.generate) elif name.startswith('I') and len(name) > 1 and name[1].isupper(): # Interface cast return self._handle_interface_cast(call, args) @@ -603,6 +577,15 @@ def _handle_type_cast_call(self, call: FunctionCall, name: str, args: str) -> Op return None + @staticmethod + def _is_primitive_cast_name(name: str) -> bool: + return ( + name in ('address', 'bool', 'bytes', 'bytes32', 'payable', 'string') + or name.startswith('uint') + or name.startswith('int') + or (name.startswith('bytes') and name[5:].isdigit()) + ) + def _handle_interface_cast(self, call: FunctionCall, args: str) -> str: """Handle interface type cast like IEffect(address(x)). diff --git a/transpiler/codegen/imports.py b/transpiler/codegen/imports.py index 596ff760..604652ee 100644 --- a/transpiler/codegen/imports.py +++ b/transpiler/codegen/imports.py @@ -59,16 +59,16 @@ def generate(self, contract_name: str = '') -> str: lines.append(f"import {{ {', '.join(runtime_imports)} }} from '{prefix}runtime';") # Base contract imports - lines.extend(self._generate_base_contract_imports(prefix)) + lines.extend(self._generate_base_contract_imports()) # Library imports - lines.extend(self._generate_library_imports(prefix, runtime_imports)) + lines.extend(self._generate_library_imports()) # Contract type imports - lines.extend(self._generate_contract_type_imports(prefix, contract_name)) + lines.extend(self._generate_contract_type_imports(contract_name)) # Inherited struct imports - lines.extend(self._generate_inherited_struct_imports(prefix, contract_name)) + lines.extend(self._generate_inherited_struct_imports(contract_name)) # External struct imports lines.extend(self._generate_external_struct_imports(prefix)) @@ -109,7 +109,7 @@ def _build_runtime_imports(self) -> List[str]: return imports - def _generate_base_contract_imports(self, prefix: str) -> List[str]: + def _generate_base_contract_imports(self) -> List[str]: """Generate import statements for base contracts and their inherited structs.""" lines = [] for base_contract in sorted(self._ctx.base_contracts_needed): @@ -132,11 +132,7 @@ def _generate_base_contract_imports(self, prefix: str) -> List[str]: lines.append(f"import {{ {base_contract} }} from '{import_path}';") return lines - def _generate_library_imports( - self, - prefix: str, - runtime_imports: List[str] - ) -> List[str]: + def _generate_library_imports(self) -> List[str]: """Generate import statements for library contracts.""" lines = [] for library in sorted(self._ctx.libraries_referenced): @@ -148,11 +144,7 @@ def _generate_library_imports( lines.append(f"import {{ {singleton_name} }} from '{import_path}';") return lines - def _generate_contract_type_imports( - self, - prefix: str, - contract_name: str - ) -> List[str]: + def _generate_contract_type_imports(self, contract_name: str) -> List[str]: """Generate import statements for contracts used as types.""" lines = [] for contract in sorted(self._ctx.contracts_referenced): @@ -162,11 +154,7 @@ def _generate_contract_type_imports( lines.append(f"import {{ {contract} }} from '{import_path}';") return lines - def _generate_inherited_struct_imports( - self, - prefix: str, - contract_name: str - ) -> List[str]: + def _generate_inherited_struct_imports(self, contract_name: str) -> List[str]: """Generate import statements for inherited structs.""" lines = [] diff --git a/transpiler/codegen/statement.py b/transpiler/codegen/statement.py index f20eacf1..c3add2ab 100644 --- a/transpiler/codegen/statement.py +++ b/transpiler/codegen/statement.py @@ -568,8 +568,22 @@ def generate_return_statement(self, stmt: ReturnStatement) -> str: def generate_delete_statement(self, stmt: DeleteStatement) -> str: """Generate TypeScript code for a delete statement.""" expr = self._expr.generate(stmt.expression) + default_value = self._get_delete_default(stmt.expression) + if default_value is not None: + return f'{self.indent()}{expr} = {default_value};' return f'{self.indent()}delete {expr};' + def _get_delete_default(self, expr: Expression) -> Optional[str]: + """Return Solidity's zero/default value for a delete target if known.""" + type_info = self._resolve_access_type(expr) + if not type_info: + return None + ts_type = self._type_converter.solidity_type_to_ts(type_info) + default_value = self._type_converter.default_value(ts_type, type_info) + if default_value == 'undefined as any': + return None + return default_value + # ========================================================================= # EMIT / REVERT # ========================================================================= diff --git a/transpiler/docs/extending.md b/transpiler/docs/extending.md index fd44e676..029e2f71 100644 --- a/transpiler/docs/extending.md +++ b/transpiler/docs/extending.md @@ -42,7 +42,7 @@ wire it into codegen, add a test. 1. In `runtime/base.ts`, define `SolidityRevertError extends Error` carrying `reason: string` and `data: string`. 2. In `codegen/expression.py`, change the `require` lowering from - `throw new Error("Require failed")` to `throw new SolidityRevertError()`. + `throw new Error(...)` to `throw new SolidityRevertError()`. Do the same for `RevertStatement` in `codegen/statement.py`, and for `revert CustomError(...)` encode the error selector + args into `data`. 3. Register `SolidityRevertError` in `ImportGenerator`'s runtime imports so diff --git a/transpiler/docs/init.md b/transpiler/docs/init.md index 518dd536..0b36cc9c 100644 --- a/transpiler/docs/init.md +++ b/transpiler/docs/init.md @@ -4,9 +4,10 @@ need, and writes a starter `transpiler-config.json`. ```bash -python3 /path/to/extruder/sol2ts.py init src/ +cd /path/to/extruder +python3 -m transpiler init /path/to/your/foundry/project/src # or non-interactive: -python3 /path/to/extruder/sol2ts.py init src/ --yes +python3 -m transpiler init /path/to/your/foundry/project/src --yes ``` ## What it produces diff --git a/transpiler/docs/quickstart.md b/transpiler/docs/quickstart.md index b4dbcd55..9e729ae6 100644 --- a/transpiler/docs/quickstart.md +++ b/transpiler/docs/quickstart.md @@ -1,6 +1,8 @@ # Quickstart -You have a Foundry project with contracts in `src/`. `extruder` can generate files in `ts-output/`, a scaffolded config, and a test file that calls into the transpiled contracts. +You have a Foundry project with contracts in `src/`. `extruder` can generate +TypeScript files in `ts-output/`, plus a scaffolded config and runtime +replacement stubs for Solidity files that need hand-written TypeScript. ## 1. Install @@ -8,9 +10,14 @@ Clone extruder somewhere ```bash git clone ~/tools/extruder -cd ~/tools/extruder && pip install -r requirements.txt +cd ~/tools/extruder +python3 -m transpiler --help ``` +Run extruder with `python3 -m transpiler` from the directory that contains the +`transpiler/` package. Running `transpiler/sol2ts.py` directly will fail because +the package uses relative imports. + Add the JS runtime deps to your own Foundry project: ```bash @@ -21,7 +28,8 @@ npm install -D viem vitest ## 2. Bootstrap your config with `extruder init` ```bash -python3 ~/tools/extruder/sol2ts.py init src/ --yes +cd ~/tools/extruder +python3 -m transpiler init /path/to/your/foundry/project/src --yes ``` This scans `src/` and writes: @@ -41,7 +49,7 @@ Drop `--yes` to run interactively. For each file under `runtime-replacements/`, replace the `throw new Error('Not implemented…')` bodies with real logic. See [`runtime-replacements.md`](runtime-replacements.md) for common patterns and -the ECDSA reference implementation. +the runtime replacement examples. You can defer this step if you want to see the transpiler run end-to-end — the stubs throw loudly at call time, so missing implementations fail fast @@ -50,7 +58,10 @@ rather than silently. ## 4. Transpile ```bash -python3 ~/tools/extruder/sol2ts.py src/ -o ts-output -d src --emit-metadata +python3 -m transpiler /path/to/your/foundry/project/src \ + -o /path/to/your/foundry/project/ts-output \ + -d /path/to/your/foundry/project/src \ + --emit-metadata ``` - `src/` is the input tree. diff --git a/transpiler/docs/runtime-replacements.md b/transpiler/docs/runtime-replacements.md index a410fdc7..6a7a4bb6 100644 --- a/transpiler/docs/runtime-replacements.md +++ b/transpiler/docs/runtime-replacements.md @@ -22,18 +22,23 @@ finish the job. ## Anatomy of a replacement -Here's the reference implementation: `runtime/ECDSA.ts`. It stands in for -Solady's `lib/ECDSA.sol`, which uses the `staticcall` precompile for -secp256k1 recovery. +Here's one reference implementation: `runtime/Ownable.ts`. It stands in for +Solady's `lib/Ownable.sol`, whose storage-slot-heavy Yul is better represented +by a small hand-written TypeScript class. ```ts import { Contract } from './base'; -export class ECDSA extends Contract { - static recover(hash: `0x${string}`, signature: `0x${string}`): string { - // ... real viem-backed secp256k1 recovery ... +export abstract class Ownable extends Contract { + private _owner: string = '0x0000000000000000000000000000000000000000'; + + protected _checkOwner(): void { + if (this._msg.sender !== this._owner) throw new Error('Unauthorized'); + } + + owner(): string { + return this._owner; } - // ... other signature-handling methods ... } ``` @@ -44,18 +49,19 @@ Two things to notice: same as transpiled contracts. You can write state if you want; you usually don't. 2. **No explicit registration.** The only thing connecting this file to the - transpiler is the `transpiler-config.json` entry below. The generator - writes `import { ECDSA } from '../runtime-replacements'` into every - generated file that originally imported `ECDSA.sol`. + transpiler is the `transpiler-config.json` entry below. The generator emits + a re-export stub for the replaced Solidity file, so downstream imports keep + working while resolving to your hand-written module. ## Authoring workflow ### 1. Scaffold ```bash -python3 /path/to/extruder/sol2ts.py \ - --emit-replacement-stub MyLib src/lib/MyLib.sol \ - -o runtime-replacements/MyLib.ts +cd /path/to/extruder +python3 -m transpiler \ + --emit-replacement-stub MyLib /path/to/your/foundry/project/src/lib/MyLib.sol \ + -o /path/to/your/foundry/project/runtime-replacements/MyLib.ts ``` This produces a TypeScript class with: @@ -115,8 +121,8 @@ now routes through your replacement. the rest of the contract transpiles fine. If your only concern is access control, fix it in the test harness (spoof `msg.sender`), not via a replacement. -- **Diagnostics (W002 / W003 / W004).** Degraded fidelity, but the - contract still transpiles. Audit case-by-case; a replacement is - overkill for most. +- **Skipped receive/fallback functions (W003).** Degraded fidelity, but the + contract still transpiles. Audit case-by-case; a replacement is overkill for + most. - **You just don't want this file transpiled.** Use `skipFiles` or `skipContracts` in the config; no replacement needed. diff --git a/transpiler/docs/runtime.md b/transpiler/docs/runtime.md index 6e9471b4..e43b7fc9 100644 --- a/transpiler/docs/runtime.md +++ b/transpiler/docs/runtime.md @@ -208,8 +208,8 @@ Event plumbing: ## Runtime replacements -The runtime ships one reference replacement: `ECDSA.ts`. See -[`runtime-replacements.md`](runtime-replacements.md) for the full authoring -workflow; the short version is that you scaffold via +The runtime ships reference replacements such as `Ownable.ts` and +`EnumerableSetLib.ts`. See [`runtime-replacements.md`](runtime-replacements.md) +for the full authoring workflow; the short version is that you scaffold via `--emit-replacement-stub`, fill in bodies, and register in `transpiler-config.json`. diff --git a/transpiler/docs/semantics.md b/transpiler/docs/semantics.md index c5e30a69..24e33b2d 100644 --- a/transpiler/docs/semantics.md +++ b/transpiler/docs/semantics.md @@ -9,7 +9,7 @@ extruder is a source-to-source transpiler, not an EVM. Some things are NOT suppo `Storage` class, inheritance as mixins, events). - `uint*` / `int*` → `bigint`. `address` / `bytes*` → `string`. `mapping(K => V)` → `Record` with factory-initialized defaults. - Structs → interfaces + factory. Enums → `as const` objects. + Structs → interfaces + factory. Enums → TypeScript `enum`s. - Contracts become ES classes extending a runtime `Contract` base that carries `_contractAddress`, `_storage`, `_msg`, an event emitter, and a transient-storage reset hook. Inter-contract references are plain object @@ -26,9 +26,8 @@ extruder is a source-to-source transpiler, not an EVM. Some things are NOT suppo - **Modifiers are stripped, not inlined.** Access control and `require` logic inside modifiers disappears. Diagnostic `W001` flags every occurrence so you can hand-audit. -- `try`/`catch` compiles to an empty block (`W002`). `receive`/`fallback` are - skipped (`W003`). Function pointers are coerced to `any` (`W004`). - User-defined operators are unrecognized. +- `try`/`catch` compiles to an empty block. `receive`/`fallback` are skipped + (`W003`). User-defined operators are unrecognized. ## EVM semantics it does not preserve @@ -42,10 +41,10 @@ trust a transpiled contract to behave like its on-chain counterpart: implicit bounds check after every arithmetic op. If you relied on revert-on-overflow as a safety net, add explicit casts or masks. - **`require` and `revert` are plain `throw`.** `require(cond, "msg")` becomes - `if (!cond) throw new Error("Require failed")`; `revert("msg")` and - `revert CustomError(...)` both become `throw new Error("Revert")`. Revert - reasons and custom error data are not propagated, and — per the codegen gap - above — `try/catch` can't recover from them. + `if (!cond) throw new Error("msg")`; bare `require(cond)` throws + `"Require failed"`. `revert("msg")` and `revert CustomError(...)` throw + generic `Error`s rather than Solidity-encoded revert data, and — per the + codegen gap above — `try/catch` can't recover from them. - **Division / modulo by zero** throws `RangeError`, not a Solidity panic. Still halts, different error type. - **Out-of-bounds array access** returns `undefined` rather than reverting. @@ -68,9 +67,11 @@ trust a transpiled contract to behave like its on-chain counterpart: write through automatically; plain TypeScript object references don't. Codegen emits `??=` where it detects the pattern, but hand-check anything with nested-mapping `storage` locals. -- **`delete arr[i]` splices**, not zero-writes. `arr.push()` returns - `undefined`, not the new length. Both differ from Solidity by enough to - bite rarely-tested code paths. +- **`delete` emits a best-effort default assignment** when the target type is + known, so `delete arr[i]` becomes a zero/default write instead of a sparse + JavaScript array hole. Unknown delete targets fall back to JavaScript + `delete`. `arr.push(value)` returns JavaScript's new array length, and + Solidity's storage-reference form `arr.push()` is not modeled. - **Addresses are lowercased strings**, not EIP-55 checksummed. String equality works, but anything that validates EIP-55 needs explicit normalization. diff --git a/transpiler/sol2ts.py b/transpiler/sol2ts.py index 9fa27d4d..9d2e3f9c 100644 --- a/transpiler/sol2ts.py +++ b/transpiler/sol2ts.py @@ -15,8 +15,8 @@ - Interface and contract inheritance Usage: - python3 extruder/sol2ts.py src/ - python3 extruder/sol2ts.py --emit-replacement-stub + python3 -m transpiler src/ + python3 -m transpiler --emit-replacement-stub Module layout: - lexer: Tokenization (tokens.py, lexer.py) @@ -58,6 +58,7 @@ def __init__( self.registry = TypeRegistry() self.emit_metadata = emit_metadata self.overrides_path = overrides_path + self._discovery_roots: List[Path] = [] # Metadata extraction for factory generation self.metadata_extractor = MetadataExtractor() if emit_metadata else None @@ -78,6 +79,7 @@ def __init__( if discovery_dirs: for dir_path in discovery_dirs: self.registry.discover_from_directory(dir_path) + self._remember_discovery_root(dir_path) def _load_config(self) -> None: """Load transpiler-config.json (consolidated configuration).""" @@ -119,6 +121,26 @@ def _load_config(self) -> None: def discover_types(self, directory: str, pattern: str = '**/*.sol') -> None: """Run type discovery on a directory of Solidity files.""" self.registry.discover_from_directory(directory, pattern) + self._remember_discovery_root(directory) + + def _remember_discovery_root(self, directory: str) -> None: + """Track discovery roots so transpile_file can avoid redundant discovery.""" + try: + root = Path(directory).resolve() + except (OSError, RuntimeError): + return + if root not in self._discovery_roots: + self._discovery_roots.append(root) + + def _is_covered_by_discovery(self, filepath: str) -> bool: + """Return True if a file lives under a root already type-discovered.""" + if not self._discovery_roots: + return False + try: + resolved = Path(filepath).resolve() + except (OSError, RuntimeError): + return False + return any(resolved == root or resolved.is_relative_to(root) for root in self._discovery_roots) def transpile_file(self, filepath: str, use_registry: bool = True) -> str: """Transpile a single Solidity file to TypeScript.""" @@ -134,7 +156,8 @@ def transpile_file(self, filepath: str, use_registry: bool = True) -> str: ast = parser.parse() self.parsed_files[filepath] = ast - self.registry.discover_from_ast(ast) + if not self._is_covered_by_discovery(filepath): + self.registry.discover_from_ast(ast) # Extract metadata for factory generation if self.metadata_extractor: @@ -387,7 +410,7 @@ def main(): # fails subcommand validation), so we dispatch init manually. if len(sys.argv) > 1 and sys.argv[1] == 'init': init_parser = argparse.ArgumentParser( - prog='extruder init', + prog='python3 -m transpiler init', description=( 'Scan a Solidity source tree and scaffold a starter ' 'transpiler-config.json. Classifies each file ' @@ -413,8 +436,9 @@ def main(): return parser = argparse.ArgumentParser( + prog='python3 -m transpiler', description='extruder — source-to-source Solidity → TypeScript transpiler', - epilog='Subcommands: `extruder init ` to scaffold a config for a new project.', + epilog='Subcommands: `python3 -m transpiler init ` to scaffold a config for a new project.', ) parser.add_argument('input', nargs='?', help='Input Solidity file or directory') parser.add_argument('-o', '--output', default='transpiler/ts-output', help='Output directory (or output file for --emit-replacement-stub)') @@ -423,8 +447,6 @@ def main(): help='Directory to scan for type discovery') parser.add_argument('--emit-metadata', action='store_true', help='Emit dependency manifest and factory functions') - parser.add_argument('--metadata-only', action='store_true', - help='Only emit metadata, skip TypeScript generation') parser.add_argument('--overrides', metavar='FILE', help='Path to transpiler-config.json for manual dependency mappings') parser.add_argument('--emit-replacement-stub', nargs=2, metavar=('CONTRACT', 'SOL_FILE'), @@ -455,7 +477,7 @@ def main(): input_path = Path(args.input) discovery_dirs = args.discover or ([str(input_path)] if input_path.is_dir() else [str(input_path.parent)]) - emit_metadata = args.emit_metadata or args.metadata_only + emit_metadata = args.emit_metadata overrides_path = args.overrides @@ -478,9 +500,7 @@ def main(): ts_code = transpiler.transpile_file(str(input_path)) - if args.metadata_only: - pass # Output metadata only - elif args.stdout: + if args.stdout: print(ts_code) else: output_path = Path(args.output) / input_path.with_suffix('.ts').name @@ -495,11 +515,8 @@ def main(): emit_metadata=emit_metadata, overrides_path=overrides_path, ) - transpiler.discover_types(str(input_path)) - - if not args.metadata_only: - results = transpiler.transpile_directory() - transpiler.write_output(results) + results = transpiler.transpile_directory() + transpiler.write_output(results) else: print(f"Error: {args.input} is not a valid file or directory") raise SystemExit(1) diff --git a/transpiler/transpiler-config.json b/transpiler/transpiler-config.json index fffc921d..4c1bc4aa 100644 --- a/transpiler/transpiler-config.json +++ b/transpiler/transpiler-config.json @@ -51,7 +51,7 @@ { "name": "at", "visibility": "protected", "params": ["set: AddressSet", "index: bigint"], "returns": "string" } ] } - }, + } ], "dependencyOverrides": { "DefaultRuleset": { From 48014037be3c1f5e46507eaf0bb2e25bffd9c023 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Tue, 28 Apr 2026 11:10:25 -0700 Subject: [PATCH 05/10] wip cleanup --- docs/extruder.md | 32 ++ transpiler/OPTIMIZATION_PLAN.md | 334 ++++++++++++++++++ transpiler/README.md | 25 +- transpiler/codegen/abi.py | 14 +- transpiler/codegen/base.py | 212 +----------- transpiler/codegen/diagnostics.py | 30 ++ transpiler/codegen/expression.py | 87 +---- transpiler/codegen/generator.py | 32 +- transpiler/codegen/metadata.py | 47 +-- transpiler/codegen/statement.py | 129 +------ transpiler/codegen/type_converter.py | 383 ++++++++++++++++++++- transpiler/config.py | 186 ++++++++++ transpiler/dependency_resolver/resolver.py | 18 +- transpiler/docs/configuration.md | 20 +- transpiler/docs/quickstart.md | 14 +- transpiler/docs/runtime-replacements.md | 4 + transpiler/docs/semantics.md | 11 +- transpiler/init.py | 122 +++---- transpiler/lowering.py | 83 +++++ transpiler/parser/__init__.py | 3 + transpiler/parser/visitor.py | 41 +++ transpiler/pyproject.toml | 38 ++ transpiler/sol2ts.py | 179 ++++------ transpiler/test_transpiler.py | 344 ++++++++++++++++++ 24 files changed, 1706 insertions(+), 682 deletions(-) create mode 100644 docs/extruder.md create mode 100644 transpiler/OPTIMIZATION_PLAN.md create mode 100644 transpiler/config.py create mode 100644 transpiler/lowering.py create mode 100644 transpiler/parser/visitor.py create mode 100644 transpiler/pyproject.toml diff --git a/docs/extruder.md b/docs/extruder.md new file mode 100644 index 00000000..8a1c0aaf --- /dev/null +++ b/docs/extruder.md @@ -0,0 +1,32 @@ +1) +introducing extruder. + +extruder is an **experimental** **vibe-coded** transpiler that converts Solidity into TypeScript. + +if this sounds insane to you, let me explain why. if you're already insane, feel free to jump to the end, download the package, and start extruding. + +2) +okay, so WHY do something this atrocious? + +two main reasons: gas efficiency, and observability. + +3) +traditionally, devs have been hesitant to include verbose events in smart contracts because of the extra gas costs. we've had projects like shadow or ghost logs try to fix this with "fake" logs that don't get shipped to prod. but this locks you into their infra, and it still requires you to write events at the end of the day. + +event logs are elegant, but also very blunt. you cannot substitute them for traces without emitting a ton of data + +4) +extruder lets you run your contracts anywhere you have a JS runtime. on your client, on your server, etc. importantly, it gives you an entire trace of any transaction. + +5) +how well does it work? i'm already dogfooding extruder in prod! it's used on the game client for @stompdotgg. stomp was a very events heavy game--any given round might produce 10-20 logs for stats being altered, statuses being applied, etc. etc. + +using extruder has saved around 15-20% in gas costs! + +6) +one extra bonus is having your contracts in TypeScript makes them more legible to LLMs. most agents have some JS sandbox, so they can easily fuzz and understand your integrations. + +7) +note that extrude does NOT SUPPORT ALL OF SOLIDITY OR THE EVM. for example, it uses the native Map and does NOT calculate storage slots correctly. some Yul will break! the goal here was to support a good enough fast enough subset of Solidity. + +anyway, it's in alpha here under AGPL v3. try it out! \ No newline at end of file diff --git a/transpiler/OPTIMIZATION_PLAN.md b/transpiler/OPTIMIZATION_PLAN.md new file mode 100644 index 00000000..ba8f9dfd --- /dev/null +++ b/transpiler/OPTIMIZATION_PLAN.md @@ -0,0 +1,334 @@ +# extruder Optimization Plan + +This plan tracks larger internal cleanups for making `transpiler/` easier to +ship as a standalone package while keeping the public API stable. + +Public API to preserve: + +- CLI shape: `python3 -m transpiler ...` +- `SolidityToTypeScriptTranspiler(...)` +- `transpile_file()`, `transpile_directory()`, `write_output()`, + `discover_types()`, `emit_replacement_stub()` +- `TypeScriptCodeGenerator` imports and compatibility shim methods +- Generated TypeScript class/runtime API + +## Phase 0: Current Cleanup Baseline + +Status: in progress on the current branch. + +Done: + +- Document module-based execution and remove direct-script instructions. +- Remove `--metadata-only`. +- Remove duplicate directory discovery call in CLI directory mode. +- Avoid rediscovering AST types for files already covered by discovery roots. +- Route primitive cast call lowering through `TypeConverter`. +- Generate best-effort Solidity-style `delete` default assignments. +- Fix stale docs around W002/W004, FAQ, enum output, runtime replacements, and + `delete` behavior. +- Fix bundled `transpiler-config.json` strict JSON syntax. + +Completed follow-up checks: + +- Added focused regression tests for `delete` default assignment. +- Added focused regression tests for config loading and `--overrides` behavior. +- Runtime replacements now win over `skipFiles`/`skipDirs`; this is documented + and covered by a regression test. + +## Phase 1: Parse Once, Reuse ASTs + +Status: initial implementation complete. + +Goal: stop lexing/parsing the same Solidity files in separate type-discovery, +metadata, and generation paths. + +Keep public API by introducing an internal session object, for example +`TranspileSession` or `ProjectCompilation`, owned by +`SolidityToTypeScriptTranspiler`. + +Internal responsibilities: + +- Discover source files once. +- Read file contents once. +- Parse each file into an AST once. +- Cache ASTs by resolved path. +- Build `TypeRegistry` from cached ASTs. +- Feed cached ASTs into codegen and metadata extraction. + +Likely steps: + +1. Add a private source-file collection helper that applies `skipFiles` and + `skipDirs`. +2. Add a private `_parse_file_cached(path)` helper. **Done.** +3. Change discovery to build the registry from cached ASTs when possible. + **Done.** +4. Change `transpile_directory()` to use the same cached ASTs. **Done.** +5. Keep `transpile_file()` behavior unchanged for callers that invoke it + directly. + +Current notes: + +- `SolidityToTypeScriptTranspiler` now owns an `_ast_cache`. +- Type discovery through the transpiler uses cached ASTs instead of + `TypeRegistry.discover_from_directory()`. +- `transpile_file()` reuses cached ASTs and remains callable directly. +- Regression coverage verifies directory transpilation parses each file once. +- A source-file collection helper is still worth adding before or during the + config-loader phase so skip/replacement precedence has one path. + +Success criteria: + +- Existing tests pass. +- Direct `transpile_file()` still works without a prior directory scan. +- Directory transpilation parses each included file once in the normal path. + +## Phase 2: Single Config Loader + +Status: initial implementation complete. + +Goal: make config behavior explicit and centralized. + +Create a `TranspilerConfig` dataclass/module that loads and normalizes: + +- `runtimeReplacements` +- runtime replacement classes/mixins/methods +- `dependencyOverrides` +- `interfaceAliases` +- `skipFiles` +- `skipDirs` + +It should answer questions like: + +- `should_skip_file(rel_path) -> bool` +- `should_skip_dir(rel_path) -> bool` +- `runtime_replacement_for(rel_path) -> Optional[Replacement]` +- `dependency_override(contract, param) -> ...` +- `interface_alias(name) -> ...` + +Likely steps: + +1. Move strict JSON loading and validation out of `sol2ts.py`. **Done.** +2. Reuse the same loader in `DependencyResolver`. **Done.** +3. Reuse the same loader in `init.py` merge/read paths. **Partially done: + config reads now use the shared loader; write/merge logic remains in + `init.py`.** +4. Normalize paths once, using POSIX-style relative paths. **Done for loader + consumers.** +5. Make replacement-vs-skip precedence explicit. + +Current notes: + +- Added `transpiler/config.py` with `TranspilerConfig`. +- `sol2ts.py` keeps its historical public attributes while sourcing them from + `TranspilerConfig`. +- `DependencyResolver` now loads dependency overrides and interface aliases + through the shared loader, including legacy top-level `overrides`. +- `init.py` config reads and non-destructive write merges now go through the + shared config module. +- Runtime replacements now take precedence over `skipFiles`/`skipDirs` and + avoid parsing the replaced Solidity file. +- Added focused config-loader tests for normalization, resolver integration, + config merges, `--overrides` skip behavior, and replacement precedence. + +Remaining work: + +- Consider exporting structured replacement entries instead of raw dicts once + call sites are smaller. + +Success criteria: + +- No duplicated config parsing logic. +- Invalid config produces one clear warning/error path. +- `--overrides` semantics are no longer split between codegen and factories. + +## Phase 3: Type Service + +Status: complete. + +Goal: consolidate type reasoning that was spread across generators. + +`TypeConverter` is the single class for Solidity-to-TypeScript type +conversion, defaults, type-cast emission, and the higher-level semantic +decisions (expression type resolution, mapping/index handling, ABI type +mapping) that the generators rely on. + +Success criteria: + +- Generated output stays behaviorally equivalent. +- `delete`, cast, mapping, and ABI behavior all use one source of type truth. +- Generator classes lose type-analysis helper code. + +Current notes: + +- `TypeConverter` owns conversion, defaults, semantic access resolution, + delete/mapping defaults, index conversion, and ABI type mapping. There is no + separate `TypeService` class; the earlier inheritance shim has been removed + and its methods absorbed into `TypeConverter`. +- `ExpressionGenerator` and `StatementGenerator` delegate type-driven + decisions to the converter instead of carrying local helper implementations. +- `BaseGenerator` only owns indentation, qualified-name lookup, and padding + formatters. +- `AbiTypeInferer` accepts an optional `type_converter` for shared + Solidity-to-ABI string mapping while remaining usable standalone. +- `replacement_stub.py` instantiates `TypeConverter` directly for stub + scaffolding. + +## Phase 4: Shared AST Visitor + +Goal: reduce repeated AST traversal code. + +Registry discovery, metadata extraction, diagnostics, init red-flag scanning, +and future lint passes all walk similar AST shapes. Add a small visitor utility +that supports: + +- SourceUnit +- ContractDefinition +- FunctionDefinition +- Statement trees +- Expression trees + +Likely steps: + +1. Add a minimal visitor in `parser/visitor.py` or `analysis/visitor.py`. +2. Port diagnostics scanning first; it is small and low risk. +3. Port `MetadataExtractor`. +4. Port init red-flag/MAYBE scanning. +5. Consider porting `TypeRegistry.discover_from_ast` last. + +Success criteria: + +- Traversal logic is shared. +- Each analysis pass contains only its own decisions. +- No AST node API changes required. + +Current notes: + +- Added `parser/visitor.py` with `ASTVisitor` and `iter_child_nodes`. +- Added traversal regression coverage for nested statements and expressions. +- Ported transpiler diagnostics to `AstDiagnosticVisitor`. +- Ported `MetadataExtractor` to `ASTVisitor` dispatch. +- Ported `extruder init` red-flag scanning to a visitor instead of a local + child-iterator implementation. + +Remaining work: + +- Consider porting `TypeRegistry.discover_from_ast` after a real-world + transpile comparison; it is broader and currently stable. + +## Phase 5: Lowering Layer + +Goal: simplify TypeScript emission by normalizing Solidity semantic constructs +before codegen. + +This is the largest change and should wait until parse caching, config, and +type service are stable. + +Candidate lowerings: + +- `delete target` -> typed default assignment when resolvable. +- `require` / `assert` / `revert` -> explicit throw nodes. +- Primitive casts -> normalized cast nodes. +- Interface/address casts -> registry lookup nodes. +- Low-level calls -> explicit placeholder/fallback nodes. +- Mapping default reads and nested mapping initialization. + +Public API stays the same; this is an internal AST-to-lowered-AST step between +parse and codegen. + +Success criteria: + +- Codegen modules become mostly straightforward TypeScript string emission. +- Unsupported/degraded semantics are easier to diagnose before emission. +- The lowering step can be tested independently on small AST snippets. + +Current notes: + +- Added `lowering.py` with an in-place AST transformer. +- `TypeScriptCodeGenerator.generate()` runs the lowering pass before emission. +- Primitive cast-style function calls such as `uint256(x)` and `address(x)` + lower to explicit `TypeCast` nodes. The fallback path in + `_handle_type_cast_call` was removed: primitive casts are *only* handled via + the `TypeCast` node path, so the lowering pass is now load-bearing rather + than opportunistic. +- Added focused lowering-layer regression coverage. + +Deferred candidates (require IR work, not yet started): + +- `delete` and mapping-default lowerings need typed semantic context; they + remain in `TypeConverter` for now. +- `require` / `assert` / low-level call lowerings need dedicated lowered AST + nodes or a clearer statement IR before they can be moved off the codegen + path. +- Interface/address casts and registry lookup nodes likewise. + +These deferred items are the bulk of what Phase 5 originally promised; only +primitive cast normalization has actually shipped. Picking them up requires +introducing new AST node types (e.g. `Throw`, `InterfaceCast`, +`MappingRead`) so the lowering pass has somewhere to lower *to*. + +## Phase 6: Standalone Packaging + +Goal: make `extruder` installable without changing current module execution. + +Likely steps: + +1. Add a package-specific `pyproject.toml` or root package metadata for + `extruder`. +2. Add a console script entry point: + + ```toml + [project.scripts] + extruder = "transpiler.sol2ts:main" + ``` + +3. Ensure package data includes: + - `runtime/*.ts` + - `docs/*.md` + - default `transpiler-config.json` +4. Keep `python3 -m transpiler` working. +5. Update docs to say both `extruder ...` and `python3 -m transpiler ...` are + supported once the console script exists. + +Success criteria: + +- Fresh checkout can run module CLI. +- Installed package can run `extruder`. +- Runtime files are present in built distributions. + +Current notes: + +- Added `transpiler/pyproject.toml` for the standalone `extruder` package + without changing the root project metadata. +- Declared the `extruder = "transpiler.sol2ts:main"` console script. +- Included runtime TS files, docs, `transpiler-config.json`, and `tsconfig.json` + as package data. +- Updated README and quickstart docs to show both installed `extruder ...` and + source-checkout `python3 -m transpiler ...` usage. +- Added packaging metadata regression coverage. + +## Implementation Order + +Recommended order: + +1. Phase 1: Parse once, reuse ASTs. +2. Phase 2: Single config loader. +3. Phase 3: Type service. +4. Phase 4: Shared AST visitor. +5. Phase 6: Standalone packaging. +6. Phase 5: Lowering layer. + +The lowering layer is deliberately last: it has the biggest upside, but it is +easier and safer after type/config/traversal logic has one home. + +## Test Strategy + +Keep the current Python suite as the fast baseline, then add focused tests as +each phase lands: + +- Parse-cache tests: count or instrument parse calls for directory transpile. +- Config-loader tests: valid config, invalid JSON, path normalization, + replacement-vs-skip precedence. +- Type-service tests: defaults, casts, array indexes, mapping indexes, delete. +- Visitor tests: traversal hits nested statements/expressions exactly once. +- Packaging tests: `python3 -m transpiler --help`, console script help once + packaging exists, and runtime package-data presence. diff --git a/transpiler/README.md b/transpiler/README.md index 9656b553..18aeafc8 100644 --- a/transpiler/README.md +++ b/transpiler/README.md @@ -26,12 +26,21 @@ into TypeScript you can run in Node or the browser. git clone ~/tools/extruder cd ~/tools/extruder +# Optional: install the standalone CLI in editable mode. +python3 -m pip install -e ./transpiler + # In your own Foundry project: npm install -D viem vitest ``` -Run extruder as a Python module from the directory that contains the -`transpiler/` package: +If installed, run: + +```bash +extruder --help +``` + +From a source checkout, you can also run extruder as a Python module from +the directory that contains the `transpiler/` package: ```bash cd ~/tools/extruder @@ -47,13 +56,13 @@ command: ```bash cd ~/tools/extruder -python3 -m transpiler init /path/to/your/foundry/project/src --yes +extruder init /path/to/your/foundry/project/src --yes ``` Then transpile: ```bash -python3 -m transpiler /path/to/your/foundry/project/src \ +extruder /path/to/your/foundry/project/src \ -o /path/to/your/foundry/project/ts-output \ -d /path/to/your/foundry/project/src \ --emit-metadata @@ -84,11 +93,13 @@ counterpart. ## CLI ``` -python3 -m transpiler [input] [options] -python3 -m transpiler init [--yes] [--stub-output-dir DIR] [--config-path PATH] -python3 -m transpiler --emit-replacement-stub CONTRACT SOL_FILE [-o OUTPUT] +extruder [input] [options] +extruder init [--yes] [--stub-output-dir DIR] [--config-path PATH] +extruder --emit-replacement-stub CONTRACT SOL_FILE [-o OUTPUT] ``` +`python3 -m transpiler ...` supports the same commands from a source checkout. + | Flag | Purpose | |---|---| | `input` *(positional)* | File or directory to transpile. | diff --git a/transpiler/codegen/abi.py b/transpiler/codegen/abi.py index 8e28593a..e2ce01a3 100644 --- a/transpiler/codegen/abi.py +++ b/transpiler/codegen/abi.py @@ -6,7 +6,10 @@ abi.encodePacked, etc. """ -from typing import List, Optional, Dict, Set +from typing import List, Optional, Dict, Set, TYPE_CHECKING + +if TYPE_CHECKING: + from .type_converter import TypeConverter from ..parser.ast_nodes import ( Expression, @@ -36,6 +39,7 @@ def __init__( known_interfaces: Optional[Set[str]] = None, known_struct_fields: Optional[Dict[str, Dict[str, str]]] = None, method_return_types: Optional[Dict[str, str]] = None, + type_converter: Optional['TypeConverter'] = None, ): """ Initialize the ABI type inferer. @@ -47,6 +51,7 @@ def __init__( known_interfaces: Set of known interface type names known_struct_fields: Maps struct names to their field types method_return_types: Maps method names to their return types + type_converter: Optional shared converter for Solidity→ABI type mapping """ self.var_types = var_types or {} self.known_enums = known_enums or set() @@ -54,6 +59,7 @@ def __init__( self.known_interfaces = known_interfaces or set() self.known_struct_fields = known_struct_fields or {} self.method_return_types = method_return_types or {} + self.type_converter = type_converter def infer_abi_types(self, args: List[Expression]) -> str: """ @@ -202,6 +208,9 @@ def _solidity_type_to_abi(self, type_name: str, is_array: bool = False) -> str: Single source of truth for Solidity→ABI type mapping. """ + if self.type_converter: + return self.type_converter.solidity_type_to_abi_param(type_name, is_array) + array_suffix = '[]' if is_array else '' if type_name in ('address', 'string', 'bool'): return f"{{type: '{type_name}{array_suffix}'}}" @@ -268,6 +277,9 @@ def _infer_single_packed_type(self, arg: Expression) -> str: def _get_packed_type(self, type_name: str, is_array: bool = False) -> str: """Get packed type string for a Solidity type.""" + if self.type_converter: + return self.type_converter.solidity_type_to_abi_type(type_name, is_array) + array_suffix = '[]' if is_array else '' if type_name == 'address': return f'address{array_suffix}' diff --git a/transpiler/codegen/base.py b/transpiler/codegen/base.py index c65011f7..d48f8fbc 100644 --- a/transpiler/codegen/base.py +++ b/transpiler/codegen/base.py @@ -5,21 +5,11 @@ used across all specialized generator classes in the code generation pipeline. """ -from typing import Optional, TYPE_CHECKING +from typing import TYPE_CHECKING if TYPE_CHECKING: from .context import CodeGenerationContext -from ..parser.ast_nodes import ( - Expression, - Identifier, - MemberAccess, - IndexAccess, - FunctionCall, - TypeCast, - TypeName, -) - class BaseGenerator: """ @@ -28,7 +18,6 @@ class BaseGenerator: Provides shared utilities for: - Indentation management - Type name resolution - - Expression type analysis - Value formatting """ @@ -94,202 +83,3 @@ def _to_padded_bytes32(self, val: str) -> str: hex_val = hex(int(val))[2:] return f'"0x{hex_val.zfill(64)}"' - # ========================================================================= - # EXPRESSION ANALYSIS - # ========================================================================= - - def _get_base_var_name(self, expr: Expression) -> Optional[str]: - """Extract the root variable name from an expression. - - For nested expressions like a.b.c or a[x][y], returns the root 'a'. - For `this.X` (state-variable access) returns 'X' — the state variable - is keyed by its own name in ``var_types``, not by `this`. - """ - if isinstance(expr, Identifier): - return None if expr.name == 'this' else expr.name - if isinstance(expr, MemberAccess): - if self._is_this_access(expr): - return expr.member - return self._get_base_var_name(expr.expression) - if isinstance(expr, IndexAccess): - return self._get_base_var_name(expr.base) - return None - - @staticmethod - def _is_this_access(expr: Expression) -> bool: - """True when ``expr`` is ``this.`` (state-variable access).""" - return ( - isinstance(expr, MemberAccess) - and isinstance(expr.expression, Identifier) - and expr.expression.name == 'this' - ) - - def _is_bigint_typed_identifier(self, expr: Expression) -> bool: - """Check if expression is an identifier with uint/int type (bigint in TypeScript).""" - if isinstance(expr, Identifier): - name = expr.name - if name in self._ctx.var_types: - type_name = self._ctx.var_types[name].name or '' - return type_name.startswith('uint') or type_name.startswith('int') - return False - - def _is_already_address_type(self, expr: Expression) -> bool: - """Check if expression is already an address type (doesn't need ._contractAddress). - - Returns True for expressions like msg.sender, tx.origin, etc. that are - already strings representing addresses in the TypeScript runtime. - """ - # Check for msg.sender, msg.origin patterns - if isinstance(expr, MemberAccess): - if isinstance(expr.expression, Identifier): - base_name = expr.expression.name - member = expr.member - # msg.sender is already an address string - if base_name == 'msg' and member == 'sender': - return True - # tx.origin is already an address string - if base_name == 'tx' and member == 'origin': - return True - # Check if this is a struct field that's already an address type - if base_name in self._ctx.var_types: - type_info = self._ctx.var_types[base_name] - if type_info.name and type_info.name in self._ctx.known_struct_fields: - struct_fields = self._ctx.known_struct_fields[type_info.name] - if member in struct_fields: - field_info = struct_fields[member] - field_type = field_info[0] if isinstance(field_info, tuple) else field_info - if field_type == 'address': - return True - # Check if it's a simple identifier with address type - if isinstance(expr, Identifier): - if expr.name in self._ctx.var_types: - type_info = self._ctx.var_types[expr.name] - if type_info.name == 'address': - return True - return False - - def _is_numeric_type_cast(self, expr: Expression) -> bool: - """Check if expression is a numeric type cast (uint160, uint256, etc.). - - Returns True for expressions that cast to integer types and produce bigint values. - This is used to properly handle address(uint160(...)) patterns. - """ - # Check for TypeCast to numeric types - if isinstance(expr, TypeCast): - type_name = expr.type_name.name - if type_name.startswith('uint') or type_name.startswith('int'): - return True - # Check for function call casts like uint160(x) - if isinstance(expr, FunctionCall): - if isinstance(expr.function, Identifier): - func_name = expr.function.name - if func_name.startswith('uint') or func_name.startswith('int'): - return True - return False - - def _resolve_access_type(self, expr: Expression) -> Optional[TypeName]: - """Resolve the TypeName at a given expression point. - - Descends through mapping/array/struct accesses so the returned TypeName - describes the container AT THIS LEVEL. Critical for nested access like - ``this.m[a][b]`` where the inner access needs the INNER mapping's - key_type, not the outer one's. - """ - if isinstance(expr, Identifier): - return None if expr.name == 'this' else self._ctx.var_types.get(expr.name) - if isinstance(expr, MemberAccess): - if self._is_this_access(expr): - return self._ctx.var_types.get(expr.member) - return self._resolve_struct_field_type(expr) - if isinstance(expr, IndexAccess): - container = self._resolve_access_type(expr.base) - return self._step_into_container(container) - return None - - def _resolve_struct_field_type(self, expr: MemberAccess) -> Optional[TypeName]: - """Type of a struct-field access, using ``known_struct_fields``.""" - parent_type = self._resolve_access_type(expr.expression) - if not parent_type or not parent_type.name: - return None - struct_fields = self._ctx.known_struct_fields.get(parent_type.name) - if not struct_fields: - return None - field_info = struct_fields.get(expr.member) - if not field_info: - return None - field_type, field_is_array = ( - field_info if isinstance(field_info, tuple) else (field_info, False) - ) - return self._field_info_to_type_name(field_type, field_is_array) - - @staticmethod - def _step_into_container(container: Optional[TypeName]) -> Optional[TypeName]: - """One indexing step: mapping -> value_type, array -> element type.""" - if container is None: - return None - if container.is_mapping: - return container.value_type - if container.is_array: - return TypeName( - name=container.name, - is_array=False, - is_mapping=False, - key_type=None, - value_type=None, - ) - return None - - @staticmethod - def _field_info_to_type_name(field_type: str, field_is_array: bool) -> Optional[TypeName]: - """Best-effort TypeName for a struct field entry from ``known_struct_fields``. - - The registry stores field types as strings; full AST TypeNames aren't - retained. We reconstruct enough for downstream access-type resolution: - mappings get ``is_mapping=True`` with a numeric key (Solidity mappings - on struct fields always use primitive keys in this codebase) and arrays - get ``is_array=True``. - """ - if not field_type: - return None - if field_type.startswith('mapping'): - return TypeName( - name=field_type, - is_mapping=True, - key_type=TypeName(name='uint256'), - value_type=TypeName(name='uint256'), - ) - return TypeName(name=field_type, is_array=field_is_array) - - def _is_likely_array_access(self, access: IndexAccess) -> bool: - """Determine if this is an array access (needs Number index) vs mapping access. - - Uses type registry for accurate detection instead of name heuristics. - """ - # Get the base variable name to look up its type - base_var_name = self._get_base_var_name(access.base) - - if base_var_name and base_var_name in self._ctx.var_types: - type_info = self._ctx.var_types[base_var_name] - # Check the type - arrays need Number(), mappings don't - if type_info.is_array: - return True - if type_info.is_mapping: - return False - - # For member access (e.g., config.p0States[j]), check if the member type is array - if isinstance(access.base, MemberAccess): - # The member access itself may be accessing an array field in a struct - # Without full struct type info, use the index type as a hint - pass - - # Fallback: check if index is a known integer type variable - if isinstance(access.index, Identifier): - index_name = access.index.name - if index_name in self._ctx.var_types: - index_type = self._ctx.var_types[index_name] - # If index is declared as uint/int, it's likely an array access - if index_type.name and (index_type.name.startswith('uint') or index_type.name.startswith('int')): - return True - - return False - diff --git a/transpiler/codegen/diagnostics.py b/transpiler/codegen/diagnostics.py index 4b06d578..52aa5cc8 100644 --- a/transpiler/codegen/diagnostics.py +++ b/transpiler/codegen/diagnostics.py @@ -11,6 +11,9 @@ from enum import Enum from typing import List, Optional +from ..parser.ast_nodes import FunctionDefinition, ModifierDefinition, SourceUnit +from ..parser.visitor import ASTVisitor + class DiagnosticSeverity(Enum): """Severity levels for transpiler diagnostics.""" @@ -240,3 +243,30 @@ def get_summary(self) -> str: parts = [f'{count} {construct}' for construct, count in sorted(by_construct.items())] return f'Transpiler warnings: {", ".join(parts)}' + + +class AstDiagnosticVisitor(ASTVisitor): + """Collect diagnostics from a parsed AST.""" + + def __init__(self, diagnostics: TranspilerDiagnostics, file_path: str = ''): + self.diagnostics = diagnostics + self.file_path = file_path + + def visit_ModifierDefinition(self, node: ModifierDefinition): + self.diagnostics.warn_modifier_stripped(node.name, file_path=self.file_path) + return self.generic_visit(node) + + def visit_FunctionDefinition(self, node: FunctionDefinition): + for mod_name in node.modifiers: + name = mod_name if isinstance(mod_name, str) else str(mod_name) + self.diagnostics.warn_modifier_stripped(name, file_path=self.file_path) + return self.generic_visit(node) + + +def emit_ast_diagnostics( + ast: SourceUnit, + diagnostics: TranspilerDiagnostics, + file_path: str = '', +) -> None: + """Scan an AST and emit diagnostics for degraded/unsupported constructs.""" + AstDiagnosticVisitor(diagnostics, file_path).visit(ast) diff --git a/transpiler/codegen/expression.py b/transpiler/codegen/expression.py index a26da907..ec89a0a8 100644 --- a/transpiler/codegen/expression.py +++ b/transpiler/codegen/expression.py @@ -80,6 +80,7 @@ def _get_abi_inferer(self) -> 'AbiTypeInferer': known_interfaces=self._ctx.known_interfaces, known_struct_fields=self._ctx.known_struct_fields, method_return_types=self._ctx.current_method_return_types, + type_converter=self._type_converter, ) return self._abi_inferer @@ -546,13 +547,12 @@ def _handle_special_function(self, call: FunctionCall, name: str, args: str) -> return None def _handle_type_cast_call(self, call: FunctionCall, name: str, args: str) -> Optional[str]: - """Handle type cast function calls (uint256(x), address(x), etc.).""" - if self._is_primitive_cast_name(name): - if len(call.arguments) != 1: - return args - cast = TypeCast(type_name=TypeName(name=name), expression=call.arguments[0]) - return self._type_converter.generate_type_cast(cast, self.generate) - elif name.startswith('I') and len(name) > 1 and name[1].isupper(): + """Handle non-primitive cast-like calls: interface casts, struct constructors, enum coercions. + + Primitive casts (``uint256(x)``, ``address(x)`` etc.) are normalized to ``TypeCast`` + nodes by the lowering pass and never reach this handler. + """ + if name.startswith('I') and len(name) > 1 and name[1].isupper(): # Interface cast return self._handle_interface_cast(call, args) elif name[0].isupper() and call.named_arguments: @@ -577,15 +577,6 @@ def _handle_type_cast_call(self, call: FunctionCall, name: str, args: str) -> Op return None - @staticmethod - def _is_primitive_cast_name(name: str) -> bool: - return ( - name in ('address', 'bool', 'bytes', 'bytes32', 'payable', 'string') - or name.startswith('uint') - or name.startswith('int') - or (name.startswith('bytes') and name[5:].isdigit()) - ) - def _handle_interface_cast(self, call: FunctionCall, args: str) -> str: """Handle interface type cast like IEffect(address(x)). @@ -663,7 +654,7 @@ def generate_member_access(self, access: MemberAccess) -> str: # Handle .length if member == 'length': - base_var_name = self._get_base_var_name(access.expression) + base_var_name = self._type_converter.base_var_name(access.expression) if base_var_name and base_var_name in self._ctx.var_types: type_info = self._ctx.var_types[base_var_name] type_name = type_info.name if type_info else '' @@ -694,65 +685,17 @@ def generate_index_access(self, access: IndexAccess) -> str: # `this.m[a][b]` or `config.p0States[j]`, this descends through # mappings, arrays, and struct fields so we always see the type of # the thing actually being indexed. - container = self._resolve_access_type(access.base) - is_array = bool(container and container.is_array) or self._is_likely_array_access(access) - is_numeric_keyed_mapping = bool( - container - and container.is_mapping - and container.key_type - and (container.key_type.name or '').startswith(('uint', 'int')) - ) - + is_array, is_numeric_keyed_mapping = self._type_converter.index_access_kind(access) mapping_access = is_numeric_keyed_mapping needs_conversion = is_array or mapping_access - index = self._convert_index(access, index, needs_conversion, mapping_access) - return f'{base}[{index}]' - - # Index expressions that can be safely wrapped in Number(...) / String(...). - _WRAPPABLE_INDEX = (Identifier, BinaryOperation, UnaryOperation, IndexAccess, MemberAccess) - - def _convert_index( - self, - access: IndexAccess, - index: str, - needs_conversion: bool, - mapping_access: bool, - ) -> str: - """Convert the index expression to match the underlying container's key type. - - Mappings transpile to ``Record``; their keys must preserve full - ``bigint`` precision, so we use ``String(idx)`` — ``Number(idx)`` silently - collapses distinct ``uint64`` values above 2^53 that happen to round to the - same IEEE-754 double, causing collisions. - - Real arrays use ``Number(idx)`` because TS's ``T[]`` index signature expects - a number (and bigint-derived indices are always within the safe integer - range in practice). - """ - wrap = 'String' if mapping_access else 'Number' - - # BigInt(N) where N is a numeric literal -> drop the wrapper entirely. - if index.startswith('BigInt('): - inner = index[7:-1] - if inner.isdigit(): - return inner - - # `5n` -> `5` (numeric literal in bigint form) - if isinstance(access.index, Literal) and index.endswith('n'): - return index[:-1] - - # Wrap wrappable index expressions when the container demands it, or - # always wrap bigint-typed identifiers (they'd otherwise index a Record - # with a `bigint` key, which TS rejects). - should_wrap = ( - (needs_conversion and isinstance(access.index, self._WRAPPABLE_INDEX)) - or (isinstance(access.index, Identifier) and self._is_bigint_typed_identifier(access.index)) + index = self._type_converter.convert_index( + access, + index, + needs_conversion, + mapping_access, ) - if should_wrap and not index.startswith(f'{wrap}('): - return f'{wrap}({index})' - - return index + return f'{base}[{index}]' # ========================================================================= # NEW EXPRESSIONS diff --git a/transpiler/codegen/generator.py b/transpiler/codegen/generator.py index 4ac0d1d3..fe8e9446 100644 --- a/transpiler/codegen/generator.py +++ b/transpiler/codegen/generator.py @@ -16,6 +16,7 @@ from .definition import DefinitionGenerator from .imports import ImportGenerator from .contract import ContractGenerator +from ..lowering import lower_ast from ..parser.ast_nodes import SourceUnit from ..type_system import TypeRegistry @@ -27,7 +28,7 @@ class TypeScriptCodeGenerator: This class orchestrates the generation of TypeScript code from Solidity AST by coordinating specialized generators for different concerns: - - TypeConverter: Type conversions + - TypeConverter: Type conversions and type-driven semantic decisions - ExpressionGenerator: Expressions - StatementGenerator: Statements - FunctionGenerator: Functions @@ -103,6 +104,7 @@ def generate(self, ast: SourceUnit) -> str: Returns: The generated TypeScript code as a string """ + ast = lower_ast(ast) output = [] # Reset context for this file @@ -153,33 +155,28 @@ def generate(self, ast: SourceUnit) -> str: return '\n'.join(output) # ========================================================================= - # BACKWARD COMPATIBILITY + # COMPATIBILITY SHIMS # ========================================================================= - - # Expose some context properties for backward compatibility + # Public surface preserved for callers that hold a TypeScriptCodeGenerator + # directly. New code should use the underlying ctx/specialized generators. @property def indent_level(self) -> int: - """Get the current indentation level.""" return self._ctx.indent_level @indent_level.setter def indent_level(self, value: int): - """Set the current indentation level.""" self._ctx.indent_level = value @property def indent_str(self) -> str: - """Get the indentation string.""" return self._ctx.indent_str def indent(self) -> str: - """Return the current indentation string.""" return self._ctx.indent() @property def current_state_vars(self) -> Set[str]: - """Get the current contract's state variables.""" return self._ctx.current_state_vars @current_state_vars.setter @@ -188,7 +185,6 @@ def current_state_vars(self, value: Set[str]): @property def current_methods(self) -> Set[str]: - """Get the current contract's methods.""" return self._ctx.current_methods @current_methods.setter @@ -197,7 +193,6 @@ def current_methods(self, value: Set[str]): @property def var_types(self) -> Dict: - """Get the variable types dictionary.""" return self._ctx.var_types @var_types.setter @@ -206,60 +201,45 @@ def var_types(self, value: Dict): @property def known_structs(self) -> Set[str]: - """Get the set of known struct names.""" return self._ctx.known_structs @property def known_enums(self) -> Set[str]: - """Get the set of known enum names.""" return self._ctx.known_enums @property def known_contracts(self) -> Set[str]: - """Get the set of known contract names.""" return self._ctx.known_contracts @property def known_interfaces(self) -> Set[str]: - """Get the set of known interface names.""" return self._ctx.known_interfaces def get_qualified_name(self, name: str) -> str: - """Get the qualified name for a type.""" return self._ctx.get_qualified_name(name) - # Delegate to specialized generators for methods that might be called directly - def generate_expression(self, expr) -> str: - """Generate TypeScript expression (for backward compatibility).""" return self._expr_generator.generate(expr) def generate_statement(self, stmt) -> str: - """Generate TypeScript statement (for backward compatibility).""" return self._stmt_generator.generate(stmt) def generate_function(self, func) -> str: - """Generate TypeScript function (for backward compatibility).""" return self._func_generator.generate_function(func) def generate_struct(self, struct) -> str: - """Generate TypeScript struct (for backward compatibility).""" return self._def_generator.generate_struct(struct) def generate_enum(self, enum) -> str: - """Generate TypeScript enum (for backward compatibility).""" return self._def_generator.generate_enum(enum) def generate_contract(self, contract) -> str: - """Generate TypeScript contract (for backward compatibility).""" return self._contract_generator.generate_contract(contract) def solidity_type_to_ts(self, type_name) -> str: - """Convert Solidity type to TypeScript (for backward compatibility).""" return self._type_converter.solidity_type_to_ts(type_name) def default_value(self, ts_type: str) -> str: - """Get default value for TypeScript type (for backward compatibility).""" return self._type_converter.default_value(ts_type) # ========================================================================= diff --git a/transpiler/codegen/metadata.py b/transpiler/codegen/metadata.py index 0243bbbf..c7d1b812 100644 --- a/transpiler/codegen/metadata.py +++ b/transpiler/codegen/metadata.py @@ -14,6 +14,7 @@ FunctionDefinition, VariableDeclaration, ) +from ..parser.visitor import ASTVisitor from ..dependency_resolver import DependencyResolver @@ -29,13 +30,14 @@ def __init__(self, name: str, kind: str, file_path: str): self.is_abstract = kind == 'abstract' -class MetadataExtractor: +class MetadataExtractor(ASTVisitor): """Extracts metadata from parsed Solidity ASTs.""" def __init__(self): self.contracts: Dict[str, ContractMetadata] = {} self.interfaces: Set[str] = set() self.libraries: Set[str] = set() + self._file_path = '' def extract_from_ast(self, ast: SourceUnit, file_path: str) -> None: """Extract metadata from a parsed AST. @@ -44,29 +46,34 @@ def extract_from_ast(self, ast: SourceUnit, file_path: str) -> None: ast: The parsed SourceUnit file_path: Relative file path (without .sol extension) """ - for contract in ast.contracts: - metadata = ContractMetadata( - name=contract.name, - kind=contract.kind, - file_path=file_path - ) + previous_file_path = self._file_path + self._file_path = file_path + self.visit(ast) + self._file_path = previous_file_path + + def visit_ContractDefinition(self, contract: ContractDefinition) -> None: + metadata = ContractMetadata( + name=contract.name, + kind=contract.kind, + file_path=self._file_path, + ) - # Track contract type - if contract.kind == 'interface': - self.interfaces.add(contract.name) - elif contract.kind == 'library': - self.libraries.add(contract.name) + # Track contract type + if contract.kind == 'interface': + self.interfaces.add(contract.name) + elif contract.kind == 'library': + self.libraries.add(contract.name) - # Extract base contracts - metadata.base_contracts = list(contract.base_contracts) + # Extract base contracts + metadata.base_contracts = list(contract.base_contracts) - # Extract constructor parameters - if contract.constructor: - metadata.constructor_params = self._extract_params( - contract.constructor - ) + # Extract constructor parameters + if contract.constructor: + metadata.constructor_params = self._extract_params( + contract.constructor + ) - self.contracts[contract.name] = metadata + self.contracts[contract.name] = metadata def _extract_params( self, func: FunctionDefinition diff --git a/transpiler/codegen/statement.py b/transpiler/codegen/statement.py index c3add2ab..e83e8b3a 100644 --- a/transpiler/codegen/statement.py +++ b/transpiler/codegen/statement.py @@ -36,9 +36,7 @@ MemberAccess, Identifier, FunctionCall, - Literal, VariableDeclaration, - TypeName, ) @@ -169,7 +167,7 @@ def _generate_nested_mapping_init(self, access: IndexAccess) -> str: lines = [] # Check if this is actually a mapping (not an array) - base_var_name = self._get_base_var_name(access) + base_var_name = self._type_converter.base_var_name(access) if base_var_name and base_var_name in self._ctx.var_types: type_info = self._ctx.var_types[base_var_name] if type_info and not type_info.is_mapping: @@ -183,43 +181,11 @@ def _generate_nested_mapping_init(self, access: IndexAccess) -> str: if deeper_init: lines.append(deeper_init) - init_value = self._get_mapping_init_value(access) + init_value = self._type_converter.mapping_init_value(access) lines.append(f'{self.indent()}{base_expr} ??= {init_value};') return '\n'.join(lines) - def _get_mapping_init_value(self, access: IndexAccess) -> str: - """Determine the initialization value for a mapping access.""" - base_var_name = self._get_base_var_name(access.base) - if not base_var_name or base_var_name not in self._ctx.var_types: - return '{}' - - type_info = self._ctx.var_types[base_var_name] - if not type_info or not type_info.is_mapping: - return '{}' - - # Navigate through nested mappings to find the value type at this level - depth = 0 - current = access - while isinstance(current.base, IndexAccess): - depth += 1 - current = current.base - - value_type = type_info.value_type - for _ in range(depth): - if value_type and value_type.is_mapping: - value_type = value_type.value_type - else: - break - - if value_type: - if value_type.is_array: - return '[]' - elif value_type.is_mapping: - return '{}' - - return '{}' - # ========================================================================= # VARIABLE DECLARATIONS # ========================================================================= @@ -247,9 +213,18 @@ def generate_variable_declaration_statement(self, stmt: VariableDeclarationState return storage_init init_expr = self._expr.generate(stmt.initial_value) - init_expr = self._add_mapping_default(stmt.initial_value, ts_type, init_expr, decl.type_name) + init_expr = self._type_converter.add_mapping_default( + stmt.initial_value, + ts_type, + init_expr, + decl.type_name, + ) # bytes32 initialized with string literal: convert to hex-padded bytes32 - init_expr = self._convert_bytes32_string_literal(decl.type_name, stmt.initial_value, init_expr) + init_expr = self._type_converter.convert_bytes_string_literal( + decl.type_name, + stmt.initial_value, + init_expr, + ) init = f' = {init_expr}' else: default_val = self._type_converter.default_value(ts_type, decl.type_name) @@ -336,71 +311,6 @@ def _get_storage_init_statement( lines.append(f'{self.indent()}let {decl.name}: {ts_type} = {mapping_expr}[{key_expr}];') return '\n'.join(lines) - def _add_mapping_default( - self, - expr: Expression, - ts_type: str, - generated_expr: str, - solidity_type: Optional[TypeName] = None - ) -> str: - """Add default value for mapping reads to simulate Solidity mapping semantics.""" - if not isinstance(expr, IndexAccess): - return generated_expr - - is_mapping_read = False - - base_var_name = self._get_base_var_name(expr.base) - if base_var_name and base_var_name in self._ctx.var_types: - type_info = self._ctx.var_types[base_var_name] - is_mapping_read = type_info.is_mapping - - if isinstance(expr.base, MemberAccess): - if isinstance(expr.base.expression, Identifier) and expr.base.expression.name == 'this': - member_name = expr.base.member - if member_name in self._ctx.var_types: - type_info = self._ctx.var_types[member_name] - is_mapping_read = type_info.is_mapping - - # Check if the base identifier has a mapping type in var_types - if isinstance(expr.base, Identifier): - name = expr.base.name - if name in self._ctx.var_types: - type_info = self._ctx.var_types[name] - if type_info.is_mapping: - is_mapping_read = True - elif name in self._ctx.current_state_vars: - # State vars that aren't in var_types but are accessed with index - # are likely mappings (conservative: treat as mapping for default values) - is_mapping_read = True - - if not is_mapping_read: - return generated_expr - - default_value = self._type_converter.default_value(ts_type, solidity_type) - if default_value and default_value != 'undefined as any': - return f'({generated_expr} ?? {default_value})' - return generated_expr - - def _convert_bytes32_string_literal(self, type_name, initial_value, init_expr: str) -> str: - """Convert a string literal to hex-padded bytes32 when assigned to a bytes32 variable. - - In Solidity, `bytes32 x = "STATUS_EFFECT"` right-pads the UTF-8 bytes with zeros - to fill 32 bytes. The transpiler must produce the equivalent hex string. - """ - if (type_name and hasattr(type_name, 'name') and type_name.name - and type_name.name.startswith('bytes') - and isinstance(initial_value, Literal) and initial_value.kind == 'string'): - string_val = initial_value.value.strip('"\'') - hex_bytes = string_val.encode('utf-8').hex() - # Determine byte size (bytes32 = 32, bytes16 = 16, etc.) - size_str = type_name.name[5:] - byte_size = int(size_str) if size_str.isdigit() else 32 - hex_bytes = hex_bytes[:byte_size * 2].ljust(byte_size * 2, '0') - return f'"0x{hex_bytes}"' - return init_expr - - # _get_ts_default_value removed — consolidated into type_converter.default_value() - def _get_small_int_conversions_from_decode(self, stmt: VariableDeclarationStatement) -> List[str]: """Get list of variable names that need BigInt conversion from abi.decode. @@ -568,22 +478,11 @@ def generate_return_statement(self, stmt: ReturnStatement) -> str: def generate_delete_statement(self, stmt: DeleteStatement) -> str: """Generate TypeScript code for a delete statement.""" expr = self._expr.generate(stmt.expression) - default_value = self._get_delete_default(stmt.expression) + default_value = self._type_converter.delete_default(stmt.expression) if default_value is not None: return f'{self.indent()}{expr} = {default_value};' return f'{self.indent()}delete {expr};' - def _get_delete_default(self, expr: Expression) -> Optional[str]: - """Return Solidity's zero/default value for a delete target if known.""" - type_info = self._resolve_access_type(expr) - if not type_info: - return None - ts_type = self._type_converter.solidity_type_to_ts(type_info) - default_value = self._type_converter.default_value(ts_type, type_info) - if default_value == 'undefined as any': - return None - return default_value - # ========================================================================= # EMIT / REVERT # ========================================================================= diff --git a/transpiler/codegen/type_converter.py b/transpiler/codegen/type_converter.py index b1d66857..ca297340 100644 --- a/transpiler/codegen/type_converter.py +++ b/transpiler/codegen/type_converter.py @@ -1,32 +1,37 @@ -""" -Type conversion utilities for code generation. +"""Type reasoning for code generation. -This module provides the TypeConverter class that handles Solidity to TypeScript -type conversions during code generation, with context-awareness for tracking -imports and handling complex type scenarios. +``TypeConverter`` owns Solidity-to-TypeScript type conversion, default values, +type-cast emission, and the higher-level semantic decisions (expression type +resolution, mapping/index handling, ABI type mapping) that the generators rely +on. It is the single source of truth for type questions during emission. """ -from typing import Optional, Set, Dict, TYPE_CHECKING +from typing import Optional, Tuple, TYPE_CHECKING if TYPE_CHECKING: from .context import CodeGenerationContext from ..type_system import TypeRegistry from .base import BaseGenerator -from ..parser.ast_nodes import TypeName, Expression, Literal, TypeCast, FunctionCall, Identifier +from ..parser.ast_nodes import ( + BinaryOperation, + Expression, + FunctionCall, + Identifier, + IndexAccess, + Literal, + MemberAccess, + TypeCast, + TypeName, + UnaryOperation, +) class TypeConverter(BaseGenerator): - """ - Handles Solidity to TypeScript type conversions. + """Solidity-to-TypeScript type conversion and type-driven semantic decisions.""" - This class provides context-aware type conversion that: - - Converts Solidity types to TypeScript types - - Tracks used types for import generation - - Handles special cases like EnumerableSetLib and contract types - - Provides default values for TypeScript types - - Generates type cast expressions - """ + # Index expressions that can be safely wrapped in Number(...) / String(...). + _WRAPPABLE_INDEX = (Identifier, BinaryOperation, UnaryOperation, IndexAccess, MemberAccess) def __init__( self, @@ -381,6 +386,47 @@ def _ensure_bigint(expr: str) -> str: return expr return f'BigInt({expr})' + def _is_already_address_type(self, expr: Expression) -> bool: + """Check if expression is already an address type.""" + if isinstance(expr, MemberAccess): + if isinstance(expr.expression, Identifier): + base_name = expr.expression.name + member = expr.member + if base_name == 'msg' and member == 'sender': + return True + if base_name == 'tx' and member == 'origin': + return True + if base_name in self._ctx.var_types: + type_info = self._ctx.var_types[base_name] + if type_info.name and type_info.name in self._ctx.known_struct_fields: + struct_fields = self._ctx.known_struct_fields[type_info.name] + if member in struct_fields: + field_info = struct_fields[member] + field_type = field_info[0] if isinstance(field_info, tuple) else field_info + if field_type == 'address': + return True + + if isinstance(expr, Identifier): + if expr.name in self._ctx.var_types: + type_info = self._ctx.var_types[expr.name] + if type_info.name == 'address': + return True + return False + + @staticmethod + def _is_numeric_type_cast(expr: Expression) -> bool: + """Check if expression is a numeric type cast.""" + if isinstance(expr, TypeCast): + type_name = expr.type_name.name + if type_name.startswith('uint') or type_name.startswith('int'): + return True + if isinstance(expr, FunctionCall): + if isinstance(expr.function, Identifier): + func_name = expr.function.name + if func_name.startswith('uint') or func_name.startswith('int'): + return True + return False + def get_mapping_value_type(self, type_name: TypeName) -> Optional[str]: """Get the value type of a mapping, recursively handling nested mappings.""" if not type_name.is_mapping: @@ -405,3 +451,308 @@ def get_array_element_type(self, type_name: TypeName) -> str: value_type=type_name.value_type, ) return self.solidity_type_to_ts(element_type) + + # ========================================================================= + # EXPRESSION ANALYSIS + # ========================================================================= + + def base_var_name(self, expr: Expression) -> Optional[str]: + """Extract the root variable name from an expression. + + For nested expressions like ``a.b.c`` or ``a[x][y]``, returns ``a``. + For ``this.X`` state-variable access, returns ``X``. + """ + if isinstance(expr, Identifier): + return None if expr.name == 'this' else expr.name + if isinstance(expr, MemberAccess): + if self.is_this_access(expr): + return expr.member + return self.base_var_name(expr.expression) + if isinstance(expr, IndexAccess): + return self.base_var_name(expr.base) + return None + + @staticmethod + def is_this_access(expr: Expression) -> bool: + """True when ``expr`` is ``this.``.""" + return ( + isinstance(expr, MemberAccess) + and isinstance(expr.expression, Identifier) + and expr.expression.name == 'this' + ) + + def is_bigint_typed_identifier(self, expr: Expression) -> bool: + """True for identifiers declared as Solidity uint/int types.""" + if isinstance(expr, Identifier): + name = expr.name + if name in self._ctx.var_types: + type_name = self._ctx.var_types[name].name or '' + return type_name.startswith('uint') or type_name.startswith('int') + return False + + def resolve_access_type(self, expr: Expression) -> Optional[TypeName]: + """Resolve the ``TypeName`` at a given expression point.""" + if isinstance(expr, Identifier): + return None if expr.name == 'this' else self._ctx.var_types.get(expr.name) + if isinstance(expr, MemberAccess): + if self.is_this_access(expr): + return self._ctx.var_types.get(expr.member) + return self.resolve_struct_field_type(expr) + if isinstance(expr, IndexAccess): + container = self.resolve_access_type(expr.base) + return self.step_into_container(container) + return None + + def resolve_struct_field_type(self, expr: MemberAccess) -> Optional[TypeName]: + """Type of a struct-field access, using ``known_struct_fields``.""" + parent_type = self.resolve_access_type(expr.expression) + if not parent_type or not parent_type.name: + return None + struct_fields = self._ctx.known_struct_fields.get(parent_type.name) + if not struct_fields: + return None + field_info = struct_fields.get(expr.member) + if not field_info: + return None + field_type, field_is_array = ( + field_info if isinstance(field_info, tuple) else (field_info, False) + ) + return self.field_info_to_type_name(field_type, field_is_array) + + @staticmethod + def step_into_container(container: Optional[TypeName]) -> Optional[TypeName]: + """One indexing step: mapping -> value_type, array -> element type.""" + if container is None: + return None + if container.is_mapping: + return container.value_type + if container.is_array: + return TypeName( + name=container.name, + is_array=False, + is_mapping=False, + key_type=None, + value_type=None, + ) + return None + + @staticmethod + def field_info_to_type_name(field_type: str, field_is_array: bool) -> Optional[TypeName]: + """Best-effort ``TypeName`` for a struct field registry entry.""" + if not field_type: + return None + if field_type.startswith('mapping'): + return TypeName( + name=field_type, + is_mapping=True, + key_type=TypeName(name='uint256'), + value_type=TypeName(name='uint256'), + ) + return TypeName(name=field_type, is_array=field_is_array) + + def is_likely_array_access(self, access: IndexAccess) -> bool: + """Determine if an index access is array-like rather than mapping-like.""" + base_var_name = self.base_var_name(access.base) + + if base_var_name and base_var_name in self._ctx.var_types: + type_info = self._ctx.var_types[base_var_name] + if type_info.is_array: + return True + if type_info.is_mapping: + return False + + if isinstance(access.index, Identifier): + index_name = access.index.name + if index_name in self._ctx.var_types: + index_type = self._ctx.var_types[index_name] + if index_type.name and index_type.name.startswith(('uint', 'int')): + return True + + return False + + def is_mapping_read(self, expr: Expression) -> bool: + """Return True if an index expression reads from a mapping container.""" + if not isinstance(expr, IndexAccess): + return False + + base_var_name = self.base_var_name(expr.base) + if base_var_name and base_var_name in self._ctx.var_types: + type_info = self._ctx.var_types[base_var_name] + if type_info.is_mapping: + return True + + if isinstance(expr.base, MemberAccess): + if isinstance(expr.base.expression, Identifier) and expr.base.expression.name == 'this': + member_name = expr.base.member + if member_name in self._ctx.var_types: + type_info = self._ctx.var_types[member_name] + if type_info.is_mapping: + return True + + if isinstance(expr.base, Identifier): + name = expr.base.name + if name in self._ctx.var_types: + type_info = self._ctx.var_types[name] + if type_info.is_mapping: + return True + if name in self._ctx.current_state_vars: + # Conservative fallback for state vars whose TypeName was not + # threaded into var_types. + return True + + return False + + # ========================================================================= + # DELETE / MAPPING DEFAULTS + # ========================================================================= + + def delete_default(self, expr: Expression) -> Optional[str]: + """Return Solidity's zero/default value for a delete target if known.""" + type_info = self.resolve_access_type(expr) + if not type_info: + return None + ts_type = self.solidity_type_to_ts(type_info) + default_value = self.default_value(ts_type, type_info) + if default_value == 'undefined as any': + return None + return default_value + + def mapping_init_value(self, access: IndexAccess) -> str: + """Determine the initialization value for a mapping access.""" + base_var_name = self.base_var_name(access.base) + if not base_var_name or base_var_name not in self._ctx.var_types: + return '{}' + + type_info = self._ctx.var_types[base_var_name] + if not type_info or not type_info.is_mapping: + return '{}' + + # Navigate through nested mappings to find the value type at this level. + depth = 0 + current = access + while isinstance(current.base, IndexAccess): + depth += 1 + current = current.base + + value_type = type_info.value_type + for _ in range(depth): + if value_type and value_type.is_mapping: + value_type = value_type.value_type + else: + break + + if value_type: + if value_type.is_array: + return '[]' + if value_type.is_mapping: + return '{}' + + return '{}' + + def add_mapping_default( + self, + expr: Expression, + ts_type: str, + generated_expr: str, + solidity_type: Optional[TypeName] = None, + ) -> str: + """Add default value for mapping reads to simulate Solidity semantics.""" + if not self.is_mapping_read(expr): + return generated_expr + + default_value = self.default_value(ts_type, solidity_type) + if default_value and default_value != 'undefined as any': + return f'({generated_expr} ?? {default_value})' + return generated_expr + + @staticmethod + def convert_bytes_string_literal( + type_name: Optional[TypeName], + initial_value: Expression, + init_expr: str, + ) -> str: + """Convert string literals assigned to bytesN into right-padded hex.""" + if not ( + type_name + and getattr(type_name, 'name', '') + and type_name.name.startswith('bytes') + and isinstance(initial_value, Literal) + and initial_value.kind == 'string' + ): + return init_expr + + string_val = initial_value.value.strip('"\'') + hex_bytes = string_val.encode('utf-8').hex() + size_str = type_name.name[5:] + byte_size = int(size_str) if size_str.isdigit() else 32 + hex_bytes = hex_bytes[:byte_size * 2].ljust(byte_size * 2, '0') + return f'"0x{hex_bytes}"' + + # ========================================================================= + # INDEX CONVERSION + # ========================================================================= + + def index_access_kind(self, access: IndexAccess) -> Tuple[bool, bool]: + """Return ``(is_array, is_numeric_keyed_mapping)`` for an index access.""" + container = self.resolve_access_type(access.base) + is_array = bool(container and container.is_array) or self.is_likely_array_access(access) + is_numeric_keyed_mapping = bool( + container + and container.is_mapping + and container.key_type + and (container.key_type.name or '').startswith(('uint', 'int')) + ) + return is_array, is_numeric_keyed_mapping + + def convert_index( + self, + access: IndexAccess, + index: str, + needs_conversion: bool, + mapping_access: bool, + ) -> str: + """Convert an index expression to match the container key type. + + Mappings transpile to ``Record`` and preserve bigint + precision via ``String(idx)``. Arrays use ``Number(idx)`` because TS + array indices are numeric. + """ + wrap = 'String' if mapping_access else 'Number' + + if index.startswith('BigInt('): + inner = index[7:-1] + if inner.isdigit(): + return inner + + if isinstance(access.index, Literal) and index.endswith('n'): + return index[:-1] + + should_wrap = ( + (needs_conversion and isinstance(access.index, self._WRAPPABLE_INDEX)) + or (isinstance(access.index, Identifier) and self.is_bigint_typed_identifier(access.index)) + ) + if should_wrap and not index.startswith(f'{wrap}('): + return f'{wrap}({index})' + + return index + + # ========================================================================= + # ABI TYPE MAPPING + # ========================================================================= + + def solidity_type_to_abi_param(self, type_name: str, is_array: bool = False) -> str: + """Convert a Solidity type name to a viem ABI parameter object string.""" + return f"{{type: '{self.solidity_type_to_abi_type(type_name, is_array)}'}}" + + def solidity_type_to_abi_type(self, type_name: str, is_array: bool = False) -> str: + """Convert a Solidity type name to an ABI type string.""" + array_suffix = '[]' if is_array else '' + if type_name in ('address', 'string', 'bool'): + return f'{type_name}{array_suffix}' + if type_name.startswith(('uint', 'int', 'bytes')): + return f'{type_name}{array_suffix}' + if type_name in self._ctx.known_enums: + return f'uint8{array_suffix}' + if type_name in self._ctx.known_contracts or type_name in self._ctx.known_interfaces: + return f'address{array_suffix}' + return f'uint256{array_suffix}' diff --git a/transpiler/config.py b/transpiler/config.py new file mode 100644 index 00000000..229f9ed5 --- /dev/null +++ b/transpiler/config.py @@ -0,0 +1,186 @@ +"""Shared loader for ``transpiler-config.json``. + +The config file is consumed by code generation, dependency resolution, and +``extruder init``. This module keeps schema parsing and path normalization in +one place while preserving the existing public APIs that accept a config path. +""" + +from dataclasses import dataclass, field +import json +from pathlib import Path +from typing import Dict, List, Optional, Set, Union + + +DependencyOverride = Union[str, List[str]] + + +def normalize_config_path(path: str) -> str: + """Normalize config paths to POSIX-style relative strings.""" + return str(Path(path)).replace('\\', '/') + + +def merge_config_updates( + existing: Optional[dict] = None, + *, + skip_files: Optional[List[str]] = None, + interface_aliases: Optional[Dict[str, Optional[str]]] = None, + dependency_overrides: Optional[Dict[str, Dict[str, DependencyOverride]]] = None, + runtime_replacements: Optional[List[dict]] = None, +) -> dict: + """Merge generated config updates without overwriting user choices. + + Existing aliases, dependency overrides, and replacement sources win on + conflict. New skip paths are unioned and normalized for consistent writes. + """ + existing = existing or {} + merged = dict(existing) + + merged_skip_files = sorted({ + normalize_config_path(str(path)) + for path in existing.get('skipFiles', []) + } | { + normalize_config_path(str(path)) + for path in (skip_files or []) + }) + + merged_aliases = dict(existing.get('interfaceAliases', {})) + for key, value in (interface_aliases or {}).items(): + merged_aliases.setdefault(key, value) + + merged_dep_overrides: Dict[str, Dict[str, DependencyOverride]] = { + key: dict(value) + for key, value in existing.get('dependencyOverrides', {}).items() + } + for contract_name, params in (dependency_overrides or {}).items(): + dest = merged_dep_overrides.setdefault(contract_name, {}) + for param_name, impl in params.items(): + dest.setdefault(param_name, impl) + + merged_replacements = list(existing.get('runtimeReplacements', [])) + existing_sources = { + normalize_config_path(str(entry.get('source') or '')) + for entry in merged_replacements + if isinstance(entry, dict) + } + for replacement in runtime_replacements or []: + source = normalize_config_path(str(replacement.get('source') or '')) + if not source or source in existing_sources: + continue + merged_replacements.append(replacement) + existing_sources.add(source) + + if merged_skip_files: + merged['skipFiles'] = merged_skip_files + if merged_aliases: + merged['interfaceAliases'] = merged_aliases + if merged_dep_overrides: + merged['dependencyOverrides'] = merged_dep_overrides + if merged_replacements: + merged['runtimeReplacements'] = merged_replacements + + return merged + + +@dataclass +class TranspilerConfig: + """Normalized representation of ``transpiler-config.json``.""" + + runtime_replacements: Dict[str, dict] = field(default_factory=dict) + runtime_replacement_classes: Set[str] = field(default_factory=set) + runtime_replacement_mixins: Dict[str, str] = field(default_factory=dict) + runtime_replacement_methods: Dict[str, Set[str]] = field(default_factory=dict) + skip_files: Set[str] = field(default_factory=set) + skip_dirs: Set[str] = field(default_factory=set) + dependency_overrides: Dict[str, Dict[str, DependencyOverride]] = field(default_factory=dict) + interface_aliases: Dict[str, Optional[str]] = field(default_factory=dict) + raw: dict = field(default_factory=dict) + + @classmethod + def default_path(cls) -> Path: + return Path(__file__).parent / 'transpiler-config.json' + + @classmethod + def load( + cls, + path: Optional[str | Path] = None, + *, + warn_missing: bool = False, + label: str = 'transpiler-config.json', + ) -> 'TranspilerConfig': + """Load and normalize a config file. + + Missing files return an empty config. Invalid JSON also returns an empty + config after printing a warning, matching the historical forgiving + behavior. + """ + config_path = Path(path) if path else cls.default_path() + if not config_path.exists(): + if warn_missing: + print(f"Warning: {label} not found at {config_path}") + return cls() + + try: + data = json.loads(config_path.read_text()) + except json.JSONDecodeError as e: + print(f"Warning: Failed to parse {config_path}: {e}") + return cls() + + return cls.from_dict(data) + + @classmethod + def from_dict(cls, data: dict) -> 'TranspilerConfig': + cfg = cls(raw=data) + + for replacement in data.get('runtimeReplacements', []): + source_path = normalize_config_path(replacement.get('source', '')) + if not source_path: + continue + + cfg.runtime_replacements[source_path] = replacement + + for export in replacement.get('exports', []): + cfg.runtime_replacement_classes.add(export) + + interface = replacement.get('interface', {}) + class_name = interface.get('class', '') + mixin_code = interface.get('mixin', '') + if class_name and mixin_code: + cfg.runtime_replacement_mixins[class_name] = mixin_code + + methods = interface.get('methods', []) + if class_name and methods: + cfg.runtime_replacement_methods[class_name] = { + m.get('name', '') for m in methods if m.get('name') + } + + cfg.skip_files = { + normalize_config_path(path) + for path in data.get('skipFiles', []) + } + cfg.skip_dirs = { + normalize_config_path(path).rstrip('/') + for path in data.get('skipDirs', []) + } + + # Consolidated config uses dependencyOverrides; legacy + # dependency-overrides.json used a top-level overrides key. + cfg.dependency_overrides = data.get( + 'dependencyOverrides', + data.get('overrides', {}), + ) + cfg.interface_aliases = data.get('interfaceAliases', {}) + return cfg + + def should_skip_file(self, rel_path: str) -> bool: + return normalize_config_path(rel_path) in self.skip_files + + def should_skip_dir(self, rel_path: str) -> bool: + rel = normalize_config_path(rel_path) + return any(rel == d or rel.startswith(d + '/') for d in self.skip_dirs) + + def runtime_replacement_for(self, rel_path: str) -> Optional[dict]: + rel = normalize_config_path(rel_path) + for source_pattern, replacement in self.runtime_replacements.items(): + if rel == source_pattern or rel.endswith(source_pattern): + return replacement + return None diff --git a/transpiler/dependency_resolver/resolver.py b/transpiler/dependency_resolver/resolver.py index 5a65aaa1..387bf71b 100644 --- a/transpiler/dependency_resolver/resolver.py +++ b/transpiler/dependency_resolver/resolver.py @@ -9,11 +9,10 @@ Unresolved dependencies are tracked and can be exported for user action. """ -import json -from pathlib import Path from typing import Dict, List, Optional, Set, Tuple, Union -from dataclasses import dataclass, field +from dataclasses import dataclass +from ..config import TranspilerConfig from .name_inferrer import NameInferrer @@ -100,15 +99,9 @@ def _load_overrides(self, path: str) -> None: Legacy `dependency-overrides.json` with a top-level `overrides` key is also accepted for backwards compatibility. """ - try: - with open(path, 'r') as f: - data = json.load(f) - self.overrides = data.get('dependencyOverrides', data.get('overrides', {})) - self.interface_aliases = data.get('interfaceAliases', {}) - except FileNotFoundError: - pass # No overrides file is fine - except json.JSONDecodeError as e: - print(f"Warning: Failed to parse {path}: {e}") + config = TranspilerConfig.load(path, warn_missing=False) + self.overrides = config.dependency_overrides + self.interface_aliases = config.interface_aliases def add_known_class(self, class_name: str) -> None: """Add a known concrete class.""" @@ -325,4 +318,3 @@ def export_unresolved(self, output_path: str) -> None: with open(output_path, 'w') as f: json.dump(output, f, indent=2) - diff --git a/transpiler/docs/configuration.md b/transpiler/docs/configuration.md index 37cfd9b4..fe81e711 100644 --- a/transpiler/docs/configuration.md +++ b/transpiler/docs/configuration.md @@ -36,6 +36,10 @@ this Solidity source, emit a stub that re-exports from this runtime module." Use for Solidity whose Yul the generator can't handle, or whose semantics need hand-written TypeScript. +Runtime replacements are explicit and take precedence over `skipFiles` and +`skipDirs`. If a file appears in both places, the transpiler still emits the +runtime re-export and does not parse that Solidity file. + | Field | Purpose | |---|---| | `source` | Path to the `.sol` file, relative to the source root. | @@ -104,8 +108,9 @@ you don't want in your simulator (e.g., legacy migration contracts). ## Field: `skipFiles` Files listed here are skipped at the filesystem level — never parsed, -never transpiled. Nothing from the file is available for cross-file type -discovery. Use for files that can't be parsed, files your simulator +never transpiled, unless the same file has a `runtimeReplacements` entry. +Nothing from the file is available for cross-file type discovery. Use for +files that can't be parsed, files your simulator doesn't need, or files that transpile incorrectly but also aren't worth a runtime replacement. @@ -120,6 +125,15 @@ domain-specific subtrees that don't belong in your simulator (e.g., a ## Precedence rules +File-level transpilation: + +1. `runtimeReplacements[source]` — emits a TypeScript re-export and avoids + parsing the Solidity file. +2. `skipFiles` / `skipDirs` — skips files with no runtime replacement. +3. Normal parse + code generation. + +Dependency resolution: + When multiple sources could decide a dependency: 1. `dependencyOverrides[Contract][param]` — always wins. @@ -131,4 +145,4 @@ When multiple sources could decide a dependency: 5. Unresolved — logged to `unresolved-dependencies.json`. `extruder init` runs exactly this chain dry at bootstrap time and prompts -for anything step 5 would catch. \ No newline at end of file +for anything step 5 would catch. diff --git a/transpiler/docs/quickstart.md b/transpiler/docs/quickstart.md index 9e729ae6..31c34f13 100644 --- a/transpiler/docs/quickstart.md +++ b/transpiler/docs/quickstart.md @@ -11,12 +11,14 @@ Clone extruder somewhere ```bash git clone ~/tools/extruder cd ~/tools/extruder -python3 -m transpiler --help +python3 -m pip install -e ./transpiler +extruder --help ``` -Run extruder with `python3 -m transpiler` from the directory that contains the -`transpiler/` package. Running `transpiler/sol2ts.py` directly will fail because -the package uses relative imports. +If you skip installation, run extruder with `python3 -m transpiler` from the +directory that contains the `transpiler/` package. Running +`transpiler/sol2ts.py` directly will fail because the package uses relative +imports. Add the JS runtime deps to your own Foundry project: @@ -29,7 +31,7 @@ npm install -D viem vitest ```bash cd ~/tools/extruder -python3 -m transpiler init /path/to/your/foundry/project/src --yes +extruder init /path/to/your/foundry/project/src --yes ``` This scans `src/` and writes: @@ -58,7 +60,7 @@ rather than silently. ## 4. Transpile ```bash -python3 -m transpiler /path/to/your/foundry/project/src \ +extruder /path/to/your/foundry/project/src \ -o /path/to/your/foundry/project/ts-output \ -d /path/to/your/foundry/project/src \ --emit-metadata diff --git a/transpiler/docs/runtime-replacements.md b/transpiler/docs/runtime-replacements.md index 6a7a4bb6..636f4626 100644 --- a/transpiler/docs/runtime-replacements.md +++ b/transpiler/docs/runtime-replacements.md @@ -126,3 +126,7 @@ now routes through your replacement. most. - **You just don't want this file transpiled.** Use `skipFiles` or `skipContracts` in the config; no replacement needed. + +If a source file appears in both `runtimeReplacements` and `skipFiles` or +`skipDirs`, the runtime replacement wins. Remove the replacement entry when +you mean "skip this entirely." diff --git a/transpiler/docs/semantics.md b/transpiler/docs/semantics.md index 24e33b2d..337782cc 100644 --- a/transpiler/docs/semantics.md +++ b/transpiler/docs/semantics.md @@ -114,13 +114,14 @@ src/*.sol ─► [Type Discovery] ─► [Lex] ─► [Parse] ─► [Codegen] 1. **Type discovery** — scan every `-d` root, build a cross-file registry so codegen can resolve qualified names in O(1). -2. **Lex / Parse** — recursive-descent parser, one AST per file. -3. **Codegen** — `TypeScriptCodeGenerator` orchestrates specialized emitters: - `TypeConverter`, `ExpressionGenerator`, `StatementGenerator`, +2. **Runtime replacement check** — files listed in `transpiler-config.json` + emit a re-export stub instead of parsed/generated output. +3. **Lex / Parse** — recursive-descent parser, one cached AST per generated + file. +4. **Codegen** — `TypeScriptCodeGenerator` orchestrates specialized emitters: + `TypeService` / `TypeConverter`, `ExpressionGenerator`, `StatementGenerator`, `FunctionGenerator`, `DefinitionGenerator`, `ContractGenerator`, `ImportGenerator`, `YulTranspiler`, `AbiTypeInferer`. -4. **Runtime replacement check** — files listed in `transpiler-config.json` - emit a re-export stub instead of transpiled output. 5. **Metadata + factories** (optional) — `MetadataExtractor` records each contract's constructor parameter types, `DependencyResolver` maps interface params to concrete implementations (via config overrides and naming diff --git a/transpiler/init.py b/transpiler/init.py index 7d3126a9..b6df5ba1 100644 --- a/transpiler/init.py +++ b/transpiler/init.py @@ -31,24 +31,19 @@ from .lexer import Lexer from .parser import Parser, SourceUnit +from .parser.visitor import ASTVisitor from .parser.ast_nodes import ( ASTNode, AssemblyBlock, AssemblyStatement, - Block, ContractDefinition, - Expression, - ExpressionStatement, FunctionCall, - FunctionDefinition, Identifier, - IfStatement, MemberAccess, NewExpression, - ReturnStatement, - VariableDeclarationStatement, ) from .codegen.metadata import MetadataExtractor +from .config import TranspilerConfig, merge_config_updates from .dependency_resolver import DependencyResolver from .type_system import TypeRegistry @@ -298,34 +293,35 @@ def _scan_contract_for_maybe(contract: ContractDefinition) -> List[str]: def _walk_for_flags(node: ASTNode, reasons: List[str]) -> None: """Recurse an AST subtree, appending red-flag reasons as we find them.""" - if node is None: - return - - # Assembly block — inspect the raw Yul source for patterns. - if isinstance(node, AssemblyBlock): - _inspect_yul(node.code, reasons) - return - if isinstance(node, AssemblyStatement): - _inspect_yul(node.block.code, reasons) - return - - # Solidity-level function calls. - if isinstance(node, FunctionCall): - _inspect_call(node, reasons) - - # Solidity-level `new Foo(...)` — only flag contract deployment, not - # array allocation (`new bytes(len)`, `new uint[](size)`). - if isinstance(node, NewExpression): + _RedFlagVisitor(reasons).visit(node) + + +class _RedFlagVisitor(ASTVisitor): + """Collect init-time REPLACE red flags from statement/expression trees.""" + + def __init__(self, reasons: List[str]): + self.reasons = reasons + + def visit_AssemblyBlock(self, node: AssemblyBlock): + _inspect_yul(node.code, self.reasons) + + def visit_AssemblyStatement(self, node: AssemblyStatement): + _inspect_yul(node.block.code, self.reasons) + + def visit_FunctionCall(self, node: FunctionCall): + _inspect_call(node, self.reasons) + return self.generic_visit(node) + + def visit_NewExpression(self, node: NewExpression): tn = node.type_name is_array_alloc = getattr(tn, 'is_array', False) or tn.name in ( 'bytes', 'string', ) or tn.name.startswith(('uint', 'int')) if not is_array_alloc: - reasons.append(f'uses `new {tn.name}(...)` to deploy a contract (no bytecode model)') - - # Recurse into children. - for child in _iter_children(node): - _walk_for_flags(child, reasons) + self.reasons.append( + f'uses `new {tn.name}(...)` to deploy a contract (no bytecode model)' + ) + return self.generic_visit(node) def _inspect_call(call: FunctionCall, reasons: List[str]) -> None: @@ -375,19 +371,6 @@ def _inspect_yul(code: str, reasons: List[str]) -> None: ) -def _iter_children(node: ASTNode): - """Yield child AST nodes. Generic — walks any dataclass field that is an - AST node or a list of AST nodes. Keeps the scan insulated from specific - statement/expression shapes.""" - for attr in vars(node).values(): - if isinstance(attr, ASTNode): - yield attr - elif isinstance(attr, list): - for item in attr: - if isinstance(item, ASTNode): - yield item - - def _run_resolver_dry_run( registry: TypeRegistry, existing_config_path: Optional[Path], @@ -721,13 +704,7 @@ def apply( def _load_config(path: Path) -> dict: """Read a `transpiler-config.json` — empty dict if missing or malformed. Warns once on invalid JSON; writers downstream clobber the file.""" - if not path.exists(): - return {} - try: - return json.loads(path.read_text()) - except json.JSONDecodeError: - print(f'Warning: {path} is not valid JSON; will overwrite.') - return {} + return TranspilerConfig.load(path, warn_missing=False).raw def _write_config( @@ -739,44 +716,23 @@ def _write_config( if existing is None: existing = _load_config(path) - # Non-destructive merge: union skipFiles, union interfaceAliases. - # runtimeReplacements entries are added for each replace target. - skip_files = sorted(set(existing.get('skipFiles', [])) | set(plan.skip_files)) - - interface_aliases = dict(existing.get('interfaceAliases', {})) - for k, v in plan.interface_aliases.items(): - interface_aliases.setdefault(k, v) # existing wins on conflict - - runtime_replacements = list(existing.get('runtimeReplacements', [])) - existing_sources = {r.get('source') for r in runtime_replacements} - for rel_path, contract_name in plan.replace_targets: - if rel_path in existing_sources: - continue - runtime_replacements.append({ + runtime_replacements = [ + { 'source': rel_path, 'reason': 'TODO: explain why this Solidity source needs a runtime replacement', 'runtimeModule': '../runtime-replacements', 'exports': [contract_name], - }) + } + for rel_path, contract_name in plan.replace_targets + ] - # Dependency overrides: existing wins on the (contract, param) key pair. - dep_overrides: Dict[str, Dict[str, object]] = { - k: dict(v) for k, v in existing.get('dependencyOverrides', {}).items() - } - for contract_name, params in plan.dependency_overrides.items(): - dest = dep_overrides.setdefault(contract_name, {}) - for param_name, impl in params.items(): - dest.setdefault(param_name, impl) - - merged = dict(existing) - if skip_files: - merged['skipFiles'] = skip_files - if interface_aliases: - merged['interfaceAliases'] = interface_aliases - if dep_overrides: - merged['dependencyOverrides'] = dep_overrides - if runtime_replacements: - merged['runtimeReplacements'] = runtime_replacements + merged = merge_config_updates( + existing, + skip_files=plan.skip_files, + interface_aliases=plan.interface_aliases, + dependency_overrides=plan.dependency_overrides, + runtime_replacements=runtime_replacements, + ) path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(merged, indent=2) + '\n') diff --git a/transpiler/lowering.py b/transpiler/lowering.py new file mode 100644 index 00000000..88eff9de --- /dev/null +++ b/transpiler/lowering.py @@ -0,0 +1,83 @@ +"""Internal AST lowering before TypeScript code generation. + +Lowering normalizes Solidity constructs into a smaller set of AST shapes while +preserving the public parser/codegen APIs. The first pass is intentionally +small: primitive cast-style function calls become explicit ``TypeCast`` nodes. +""" + +from dataclasses import fields, is_dataclass + +from .parser.ast_nodes import ( + ASTNode, + FunctionCall, + Identifier, + TypeCast, + TypeName, +) + + +PRIMITIVE_CAST_NAMES = { + 'address', + 'bool', + 'bytes', + 'bytes32', + 'payable', + 'string', +} + + +def lower_ast(ast: ASTNode) -> ASTNode: + """Lower an AST in place and return it.""" + return SolidityLowerer().visit(ast) + + +def is_primitive_cast_name(name: str) -> bool: + return ( + name in PRIMITIVE_CAST_NAMES + or name.startswith('uint') + or name.startswith('int') + or (name.startswith('bytes') and name[5:].isdigit()) + ) + + +class SolidityLowerer: + """Small in-place AST transformer.""" + + def visit(self, node): + if node is None: + return None + if isinstance(node, ASTNode): + method = getattr(self, f'visit_{type(node).__name__}', self.generic_visit) + return method(node) + return node + + def generic_visit(self, node: ASTNode): + if not is_dataclass(node): + return node + for field in fields(node): + setattr(node, field.name, self._lower_value(getattr(node, field.name))) + return node + + def _lower_value(self, value): + if isinstance(value, ASTNode): + return self.visit(value) + if isinstance(value, list): + return [self._lower_value(item) for item in value] + if isinstance(value, tuple): + return tuple(self._lower_value(item) for item in value) + if isinstance(value, dict): + return {key: self._lower_value(item) for key, item in value.items()} + return value + + def visit_FunctionCall(self, node: FunctionCall): + self.generic_visit(node) + if not isinstance(node.function, Identifier): + return node + if node.named_arguments or node.call_options: + return node + if len(node.arguments) != 1: + return node + name = node.function.name + if not is_primitive_cast_name(name): + return node + return TypeCast(type_name=TypeName(name=name), expression=node.arguments[0]) diff --git a/transpiler/parser/__init__.py b/transpiler/parser/__init__.py index a4300a9a..22ca1e2d 100644 --- a/transpiler/parser/__init__.py +++ b/transpiler/parser/__init__.py @@ -58,6 +58,7 @@ AssemblyStatement, ) from .parser import Parser +from .visitor import ASTVisitor, iter_child_nodes __all__ = [ # Base @@ -113,4 +114,6 @@ 'AssemblyStatement', # Parser 'Parser', + 'ASTVisitor', + 'iter_child_nodes', ] diff --git a/transpiler/parser/visitor.py b/transpiler/parser/visitor.py new file mode 100644 index 00000000..65cdb0fc --- /dev/null +++ b/transpiler/parser/visitor.py @@ -0,0 +1,41 @@ +"""Small generic visitor for Solidity AST nodes.""" + +from dataclasses import fields, is_dataclass +from typing import Iterator + +from .ast_nodes import ASTNode + + +def iter_child_nodes(node: ASTNode) -> Iterator[ASTNode]: + """Yield AST children from dataclass fields, lists, tuples, and dict values.""" + if not is_dataclass(node): + return + + for field in fields(node): + yield from _iter_node_values(getattr(node, field.name)) + + +def _iter_node_values(value) -> Iterator[ASTNode]: + if isinstance(value, ASTNode): + yield value + elif isinstance(value, (list, tuple)): + for item in value: + yield from _iter_node_values(item) + elif isinstance(value, dict): + for item in value.values(): + yield from _iter_node_values(item) + + +class ASTVisitor: + """Pre-order visitor with ``visit_`` dispatch.""" + + def visit(self, node: ASTNode): + if node is None: + return None + method = getattr(self, f'visit_{type(node).__name__}', self.generic_visit) + return method(node) + + def generic_visit(self, node: ASTNode): + for child in iter_child_nodes(node): + self.visit(child) + return None diff --git a/transpiler/pyproject.toml b/transpiler/pyproject.toml new file mode 100644 index 00000000..2659eb48 --- /dev/null +++ b/transpiler/pyproject.toml @@ -0,0 +1,38 @@ +[build-system] +requires = ["setuptools>=69"] +build-backend = "setuptools.build_meta" + +[project] +name = "extruder" +version = "0.1.0" +description = "Source-to-source Solidity to TypeScript transpiler" +readme = "README.md" +requires-python = ">=3.11" +license = { text = "AGPL-3.0" } +authors = [ + { name = "Chomp" } +] + +[project.scripts] +extruder = "transpiler.sol2ts:main" + +[tool.setuptools] +packages = [ + "transpiler", + "transpiler.codegen", + "transpiler.dependency_resolver", + "transpiler.lexer", + "transpiler.parser", + "transpiler.type_system", +] + +[tool.setuptools.package-dir] +transpiler = "." + +[tool.setuptools.package-data] +transpiler = [ + "docs/*.md", + "runtime/*.ts", + "transpiler-config.json", + "tsconfig.json", +] diff --git a/transpiler/sol2ts.py b/transpiler/sol2ts.py index 9d2e3f9c..81330bbc 100644 --- a/transpiler/sol2ts.py +++ b/transpiler/sol2ts.py @@ -26,7 +26,6 @@ - dependency_resolver: Interface → concrete implementation resolution """ -import json import shutil from pathlib import Path from typing import Optional, List, Dict, Set @@ -37,7 +36,8 @@ from .type_system import TypeRegistry from .codegen import TypeScriptCodeGenerator from .codegen.metadata import MetadataExtractor, FactoryGenerator -from .codegen.diagnostics import TranspilerDiagnostics +from .codegen.diagnostics import TranspilerDiagnostics, emit_ast_diagnostics +from .config import TranspilerConfig, normalize_config_path from .dependency_resolver import DependencyResolver @@ -55,6 +55,7 @@ def __init__( self.source_dir = Path(source_dir) self.output_dir = Path(output_dir) self.parsed_files: Dict[str, SourceUnit] = {} + self._ast_cache: Dict[str, SourceUnit] = {} self.registry = TypeRegistry() self.emit_metadata = emit_metadata self.overrides_path = overrides_path @@ -67,60 +68,34 @@ def __init__( self.diagnostics = TranspilerDiagnostics() # Load consolidated transpiler configuration - self.runtime_replacements: Dict[str, dict] = {} - self.runtime_replacement_classes: Set[str] = set() - self.runtime_replacement_mixins: Dict[str, str] = {} - self.runtime_replacement_methods: Dict[str, Set[str]] = {} - self.skip_files: Set[str] = set() - self.skip_dirs: Set[str] = set() - self._load_config() + config_path = self.overrides_path or TranspilerConfig.default_path() + self.config = TranspilerConfig.load(config_path, warn_missing=True) + self.runtime_replacements: Dict[str, dict] = self.config.runtime_replacements + self.runtime_replacement_classes: Set[str] = self.config.runtime_replacement_classes + self.runtime_replacement_mixins: Dict[str, str] = self.config.runtime_replacement_mixins + self.runtime_replacement_methods: Dict[str, Set[str]] = self.config.runtime_replacement_methods + self.skip_files: Set[str] = self.config.skip_files + self.skip_dirs: Set[str] = self.config.skip_dirs # Run type discovery on specified directories if discovery_dirs: for dir_path in discovery_dirs: - self.registry.discover_from_directory(dir_path) - self._remember_discovery_root(dir_path) - - def _load_config(self) -> None: - """Load transpiler-config.json (consolidated configuration).""" - config_file = Path(__file__).parent / 'transpiler-config.json' - if not config_file.exists(): - print(f"Warning: transpiler-config.json not found at {config_file}") - return - - try: - with open(config_file, 'r') as f: - config = json.load(f) - - # Runtime replacements - for replacement in config.get('runtimeReplacements', []): - source_path = replacement.get('source', '') - if source_path: - self.runtime_replacements[source_path] = replacement - for export in replacement.get('exports', []): - self.runtime_replacement_classes.add(export) - interface = replacement.get('interface', {}) - class_name = interface.get('class', '') - mixin_code = interface.get('mixin', '') - if class_name and mixin_code: - self.runtime_replacement_mixins[class_name] = mixin_code - methods = interface.get('methods', []) - if class_name and methods: - method_names = set(m.get('name', '') for m in methods if m.get('name')) - self.runtime_replacement_methods[class_name] = method_names - - # Skip files and directories - for path in config.get('skipFiles', []): - self.skip_files.add(path) - for path in config.get('skipDirs', []): - self.skip_dirs.add(path) - - except (json.JSONDecodeError, KeyError) as e: - print(f"Warning: Failed to load transpiler-config.json: {e}") + self._discover_from_directory_cached(dir_path) def discover_types(self, directory: str, pattern: str = '**/*.sol') -> None: """Run type discovery on a directory of Solidity files.""" - self.registry.discover_from_directory(directory, pattern) + self._discover_from_directory_cached(directory, pattern) + + def _discover_from_directory_cached(self, directory: str, pattern: str = '**/*.sol') -> None: + """Discover types from Solidity files while reusing parsed ASTs.""" + base_dir = Path(directory) + for sol_file in base_dir.glob(pattern): + try: + rel_path = sol_file.relative_to(base_dir).with_suffix('') + ast = self._parse_file_cached(sol_file) + self.registry.discover_from_ast(ast, str(rel_path)) + except Exception as e: + print(f"Warning: Could not parse {sol_file} for type discovery: {e}") self._remember_discovery_root(directory) def _remember_discovery_root(self, directory: str) -> None: @@ -142,40 +117,36 @@ def _is_covered_by_discovery(self, filepath: str) -> bool: return False return any(resolved == root or resolved.is_relative_to(root) for root in self._discovery_roots) - def transpile_file(self, filepath: str, use_registry: bool = True) -> str: - """Transpile a single Solidity file to TypeScript.""" - with open(filepath, 'r') as f: - source = f.read() + def _cache_key(self, filepath: str | Path) -> str: + """Stable key for source/AST caches.""" + try: + return str(Path(filepath).resolve()) + except (OSError, RuntimeError): + return str(Path(filepath)) + + def _parse_file_cached(self, filepath: str | Path) -> SourceUnit: + """Read, lex, and parse a Solidity file once per transpiler instance.""" + cache_key = self._cache_key(filepath) + if cache_key in self._ast_cache: + return self._ast_cache[cache_key] - # Tokenize using the lexer module + source = Path(filepath).read_text() lexer = Lexer(source) tokens = lexer.tokenize() - - # Parse using the parser module parser = Parser(tokens) ast = parser.parse() - self.parsed_files[filepath] = ast - if not self._is_covered_by_discovery(filepath): - self.registry.discover_from_ast(ast) - - # Extract metadata for factory generation - if self.metadata_extractor: - try: - resolved_filepath = Path(filepath).resolve() - resolved_source_dir = self.source_dir.resolve() - if resolved_filepath.is_relative_to(resolved_source_dir): - rel_path = resolved_filepath.relative_to(resolved_source_dir) - file_path_no_ext = str(rel_path.with_suffix('')) - else: - file_path_no_ext = Path(filepath).stem - self.metadata_extractor.extract_from_ast(ast, file_path_no_ext) - except (ValueError, TypeError, AttributeError): - pass + self._ast_cache[cache_key] = ast + self.parsed_files[str(filepath)] = ast + return ast - # Calculate file depth for imports + def transpile_file(self, filepath: str, use_registry: bool = True) -> str: + """Transpile a single Solidity file to TypeScript.""" + # Calculate file depth for imports before parsing so runtime + # replacements can stand in for files the parser cannot handle. file_depth = 0 current_file_path = '' + rel_path: Optional[Path] = None try: resolved_filepath = Path(filepath).resolve() resolved_source_dir = self.source_dir.resolve() @@ -186,15 +157,31 @@ def transpile_file(self, filepath: str, use_registry: bool = True) -> str: except (ValueError, TypeError, AttributeError): pass - # Emit diagnostics for skipped constructs in the AST - self._emit_ast_diagnostics(ast, filepath) - - # Check for runtime replacement replacement = self._get_runtime_replacement(filepath) if replacement: - self.diagnostics.info_runtime_replacement(filepath, replacement.get('runtime', '')) + runtime_module = replacement.get('runtimeModule', replacement.get('runtime', '')) + self.diagnostics.info_runtime_replacement(filepath, runtime_module) return self._generate_runtime_reexport(replacement, file_depth) + ast = self._parse_file_cached(filepath) + self.parsed_files[filepath] = ast + if not self._is_covered_by_discovery(filepath): + self.registry.discover_from_ast(ast) + + # Extract metadata for factory generation + if self.metadata_extractor: + try: + if rel_path is not None: + file_path_no_ext = str(rel_path.with_suffix('')) + else: + file_path_no_ext = Path(filepath).stem + self.metadata_extractor.extract_from_ast(ast, file_path_no_ext) + except (ValueError, TypeError, AttributeError): + pass + + # Emit diagnostics for skipped constructs in the AST + self._emit_ast_diagnostics(ast, filepath) + # Generate TypeScript using the modular code generator generator = TypeScriptCodeGenerator( self.registry if use_registry else None, @@ -208,36 +195,17 @@ def transpile_file(self, filepath: str, use_registry: bool = True) -> str: def _emit_ast_diagnostics(self, ast: SourceUnit, filepath: str) -> None: """Scan the AST and emit diagnostics for skipped/unsupported constructs.""" - for contract in ast.contracts: - # Check for modifiers (they are parsed but not inlined) - for modifier in contract.modifiers: - self.diagnostics.warn_modifier_stripped( - modifier.name, - file_path=filepath, - ) - - # Check for function modifiers referenced on functions - for func in contract.functions: - if func.modifiers: - for mod_name in func.modifiers: - name = mod_name if isinstance(mod_name, str) else str(mod_name) - self.diagnostics.warn_modifier_stripped( - name, - file_path=filepath, - ) + emit_ast_diagnostics(ast, self.diagnostics, filepath) def _get_runtime_replacement(self, filepath: str) -> Optional[dict]: """Check if a file should be replaced with a runtime implementation.""" try: rel_path = Path(filepath).relative_to(self.source_dir) - rel_str = str(rel_path).replace('\\', '/') + rel_str = normalize_config_path(str(rel_path)) except ValueError: - rel_str = str(Path(filepath)).replace('\\', '/') + rel_str = normalize_config_path(str(Path(filepath))) - for source_pattern, replacement in self.runtime_replacements.items(): - if rel_str.endswith(source_pattern) or rel_str == source_pattern: - return replacement - return None + return self.config.runtime_replacement_for(rel_str) def _generate_runtime_reexport(self, replacement: dict, file_depth: int) -> str: """Generate a re-export file for a runtime replacement.""" @@ -267,10 +235,13 @@ def transpile_directory(self, pattern: str = '**/*.sol') -> Dict[str, str]: for sol_file in self.source_dir.glob(pattern): # Check if file or directory should be skipped rel = sol_file.relative_to(self.source_dir) - if str(rel) in self.skip_files: + rel_str = normalize_config_path(str(rel)) + has_replacement = self.config.runtime_replacement_for(rel_str) is not None + if not has_replacement and self.config.should_skip_file(rel_str): continue - if any(str(rel).startswith(d + '/') or str(rel).startswith(d + '\\') for d in self.skip_dirs): + if not has_replacement and self.config.should_skip_dir(rel_str): continue + try: ts_code = self.transpile_file(str(sol_file)) rel_path = sol_file.relative_to(self.source_dir) diff --git a/transpiler/test_transpiler.py b/transpiler/test_transpiler.py index 3b492217..3e4866c1 100644 --- a/transpiler/test_transpiler.py +++ b/transpiler/test_transpiler.py @@ -671,6 +671,241 @@ def test_diagnostics_severity_levels(self): self.assertEqual(len(warnings), 1) self.assertEqual(len(infos), 1) + def test_ast_diagnostics_visitor_collects_modifier_warnings(self): + from transpiler.codegen.diagnostics import TranspilerDiagnostics, emit_ast_diagnostics + + source = ''' + contract TestContract { + modifier onlyOwner() { _; } + + function guarded() public onlyOwner { + } + } + ''' + + lexer = Lexer(source) + tokens = lexer.tokenize() + parser = Parser(tokens) + ast = parser.parse() + + diag = TranspilerDiagnostics() + emit_ast_diagnostics(ast, diag, 'TestContract.sol') + + self.assertEqual(len(diag.warnings), 2) + self.assertTrue(all(w.code == 'W001' for w in diag.warnings)) + + +class TestAstVisitor(unittest.TestCase): + """Test generic AST visitor traversal.""" + + def test_visitor_reaches_nested_statements_and_expressions(self): + from transpiler.parser.visitor import ASTVisitor + from transpiler.parser.ast_nodes import BinaryOperation, FunctionCall, Identifier + + source = ''' + contract TestContract { + function run(uint256 x) public { + if (x > 0) { + ping(x + 1); + } + } + } + ''' + + lexer = Lexer(source) + tokens = lexer.tokenize() + parser = Parser(tokens) + ast = parser.parse() + + class RecordingVisitor(ASTVisitor): + def __init__(self): + self.binary_ops = [] + self.calls = [] + + def visit_BinaryOperation(self, node: BinaryOperation): + self.binary_ops.append(node.operator) + return self.generic_visit(node) + + def visit_FunctionCall(self, node: FunctionCall): + if isinstance(node.function, Identifier): + self.calls.append(node.function.name) + return self.generic_visit(node) + + visitor = RecordingVisitor() + visitor.visit(ast) + + self.assertIn('>', visitor.binary_ops) + self.assertIn('+', visitor.binary_ops) + self.assertIn('ping', visitor.calls) + + +class TestTranspilerConfig(unittest.TestCase): + """Test the shared transpiler-config loader.""" + + def test_config_loader_normalizes_runtime_and_dependency_fields(self): + from transpiler.config import TranspilerConfig + + cfg = TranspilerConfig.from_dict({ + 'runtimeReplacements': [{ + 'source': 'lib\\Ownable.sol', + 'exports': ['Ownable'], + 'interface': { + 'class': 'Ownable', + 'methods': [{'name': 'owner'}], + 'mixin': 'mixin code', + }, + }], + 'skipFiles': ['test\\Fixture.sol'], + 'skipDirs': ['script\\deploy'], + 'dependencyOverrides': {'UsesFoo': {'_foo': 'Foo'}}, + 'interfaceAliases': {'IFoo': 'Foo'}, + }) + + self.assertTrue(cfg.runtime_replacement_for('src/lib/Ownable.sol')) + self.assertTrue(cfg.should_skip_file('test/Fixture.sol')) + self.assertTrue(cfg.should_skip_dir('script/deploy/Deploy.sol')) + self.assertEqual(cfg.dependency_overrides['UsesFoo']['_foo'], 'Foo') + self.assertEqual(cfg.interface_aliases['IFoo'], 'Foo') + self.assertIn('Ownable', cfg.runtime_replacement_classes) + self.assertEqual(cfg.runtime_replacement_methods['Ownable'], {'owner'}) + self.assertEqual(cfg.runtime_replacement_mixins['Ownable'], 'mixin code') + + def test_merge_config_preserves_existing_conflicts_and_adds_new_entries(self): + from transpiler.config import merge_config_updates + + merged = merge_config_updates( + { + 'skipFiles': ['legacy\\A.sol'], + 'interfaceAliases': {'IFoo': 'Foo'}, + 'dependencyOverrides': {'UsesFoo': {'_foo': 'OldFoo'}}, + 'runtimeReplacements': [{ + 'source': 'legacy\\Replacement.sol', + 'exports': ['OldReplacement'], + }], + }, + skip_files=['new\\B.sol', 'legacy/A.sol'], + interface_aliases={'IFoo': 'NewFoo', 'IBar': 'Bar'}, + dependency_overrides={ + 'UsesFoo': {'_foo': 'NewFoo', '_bar': 'Bar'}, + 'UsesBaz': {'_baz': ['Baz']}, + }, + runtime_replacements=[ + {'source': 'legacy/Replacement.sol', 'exports': ['Duplicate']}, + {'source': 'new/Replacement.sol', 'exports': ['NewReplacement']}, + ], + ) + + self.assertEqual(merged['skipFiles'], ['legacy/A.sol', 'new/B.sol']) + self.assertEqual(merged['interfaceAliases']['IFoo'], 'Foo') + self.assertEqual(merged['interfaceAliases']['IBar'], 'Bar') + self.assertEqual(merged['dependencyOverrides']['UsesFoo']['_foo'], 'OldFoo') + self.assertEqual(merged['dependencyOverrides']['UsesFoo']['_bar'], 'Bar') + self.assertEqual(merged['dependencyOverrides']['UsesBaz']['_baz'], ['Baz']) + self.assertEqual(len(merged['runtimeReplacements']), 2) + self.assertEqual( + merged['runtimeReplacements'][1]['source'], + 'new/Replacement.sol', + ) + + def test_dependency_resolver_uses_shared_config_loader(self): + import tempfile + from pathlib import Path + from transpiler.dependency_resolver import DependencyResolver + + with tempfile.TemporaryDirectory() as td: + path = Path(td) / 'transpiler-config.json' + path.write_text('''{ + "dependencyOverrides": {"UsesFoo": {"_foo": "Foo"}}, + "interfaceAliases": {"IBar": "Bar"} + }''') + + resolver = DependencyResolver( + overrides_path=str(path), + known_classes={'Foo', 'Bar'}, + ) + + self.assertEqual(resolver.overrides['UsesFoo']['_foo'], 'Foo') + self.assertEqual(resolver.interface_aliases['IBar'], 'Bar') + + def test_transpiler_uses_override_config_for_skip_files(self): + import tempfile + from pathlib import Path + from transpiler.sol2ts import SolidityToTypeScriptTranspiler + + with tempfile.TemporaryDirectory() as td: + tree = Path(td) + (tree / 'A.sol').write_text('contract A { function a() public {} }') + (tree / 'B.sol').write_text('contract B { function b() public {} }') + config_path = tree / 'transpiler-config.json' + config_path.write_text('{"skipFiles": ["B.sol"]}') + + transpiler = SolidityToTypeScriptTranspiler( + source_dir=str(tree), + output_dir=str(tree / 'out'), + discovery_dirs=[str(tree)], + overrides_path=str(config_path), + ) + results = transpiler.transpile_directory() + + self.assertEqual(len(results), 1) + self.assertTrue(any(path.endswith('A.ts') for path in results)) + self.assertFalse(any(path.endswith('B.ts') for path in results)) + + def test_runtime_replacement_wins_over_skip_file_and_avoids_parse(self): + import tempfile + from pathlib import Path + from transpiler.sol2ts import SolidityToTypeScriptTranspiler + + with tempfile.TemporaryDirectory() as td: + tree = Path(td) + (tree / 'Replaced.sol').write_text('contract Replaced {') + config_path = tree / 'transpiler-config.json' + config_path.write_text('''{ + "runtimeReplacements": [{ + "source": "Replaced.sol", + "runtimeModule": "../runtime-replacements", + "exports": ["Replaced"], + "reason": "test replacement" + }], + "skipFiles": ["Replaced.sol"] + }''') + + transpiler = SolidityToTypeScriptTranspiler( + source_dir=str(tree), + output_dir=str(tree / 'out'), + overrides_path=str(config_path), + ) + results = transpiler.transpile_directory() + + self.assertEqual(len(results), 1) + output = next(iter(results.values())) + self.assertIn("export { Replaced } from '../runtime-replacements';", output) + + +class TestPackaging(unittest.TestCase): + """Test standalone package metadata.""" + + def test_pyproject_declares_console_script_and_package_data(self): + import tomllib + from pathlib import Path + + project_root = Path(__file__).parent + pyproject = tomllib.loads((project_root / 'pyproject.toml').read_text()) + + self.assertEqual( + pyproject['project']['scripts']['extruder'], + 'transpiler.sol2ts:main', + ) + + package_data = set(pyproject['tool']['setuptools']['package-data']['transpiler']) + self.assertIn('runtime/*.ts', package_data) + self.assertIn('docs/*.md', package_data) + self.assertIn('transpiler-config.json', package_data) + + self.assertTrue((project_root / 'runtime' / 'index.ts').exists()) + self.assertTrue((project_root / 'docs' / 'quickstart.md').exists()) + self.assertTrue((project_root / 'transpiler-config.json').exists()) + class TestStructDefaultValues(unittest.TestCase): """Test struct default value generation.""" @@ -843,6 +1078,115 @@ def test_address_cast(self): self.assertIn('getAddr', output) +class TestLoweringLayer(unittest.TestCase): + """Test pre-codegen AST lowering.""" + + def test_primitive_cast_call_lowers_to_type_cast_node(self): + from transpiler.lowering import lower_ast + from transpiler.parser.ast_nodes import ReturnStatement, TypeCast + + source = ''' + contract TestContract { + function cast(int256 x) public pure returns (uint256) { + return uint256(x); + } + } + ''' + + lexer = Lexer(source) + tokens = lexer.tokenize() + parser = Parser(tokens) + ast = parser.parse() + + lowered = lower_ast(ast) + stmt = lowered.contracts[0].functions[0].body.statements[0] + + self.assertIsInstance(stmt, ReturnStatement) + self.assertIsInstance(stmt.expression, TypeCast) + self.assertEqual(stmt.expression.type_name.name, 'uint256') + + +class TestDeleteGeneration(unittest.TestCase): + """Test Solidity delete semantics in generated TypeScript.""" + + def test_delete_array_element_zero_writes(self): + source = ''' + contract TestContract { + uint256[] values; + + function clear(uint256 index) public { + delete values[index]; + } + } + ''' + + lexer = Lexer(source) + tokens = lexer.tokenize() + parser = Parser(tokens) + ast = parser.parse() + + generator = TypeScriptCodeGenerator() + output = generator.generate(ast) + + self.assertIn('this.values[Number(index)] = 0n;', output) + self.assertNotIn('delete this.values', output) + + def test_delete_state_variable_zero_writes(self): + source = ''' + contract TestContract { + bool enabled; + + function clear() public { + delete enabled; + } + } + ''' + + lexer = Lexer(source) + tokens = lexer.tokenize() + parser = Parser(tokens) + ast = parser.parse() + + generator = TypeScriptCodeGenerator() + output = generator.generate(ast) + + self.assertIn('this.enabled = false;', output) + self.assertNotIn('delete this.enabled', output) + + +class TestTranspileDirectoryCaching(unittest.TestCase): + """Test that directory transpilation reuses parsed ASTs after discovery.""" + + def test_directory_transpile_parses_each_file_once(self): + import tempfile + from pathlib import Path + from unittest.mock import patch + from transpiler.sol2ts import SolidityToTypeScriptTranspiler, Parser as Sol2TsParser + + with tempfile.TemporaryDirectory() as td: + tree = Path(td) + (tree / 'A.sol').write_text('contract A { function a() public {} }') + (tree / 'B.sol').write_text('contract B { function b() public {} }') + + parse_count = [0] + original_parse = Sol2TsParser.parse + + def counted_parse(parser_self): + parse_count[0] += 1 + return original_parse(parser_self) + + with patch.object(Sol2TsParser, 'parse', counted_parse): + transpiler = SolidityToTypeScriptTranspiler( + source_dir=str(tree), + output_dir=str(tree / 'out'), + discovery_dirs=[str(tree)], + ) + results = transpiler.transpile_directory() + + self.assertEqual(parse_count[0], 2) + self.assertEqual(len(results), 2) + + # ============================================================================= # extruder init — scan phase # ============================================================================= From d58668f141141613575cf2c334ace5a1493355bd Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Tue, 28 Apr 2026 12:15:28 -0700 Subject: [PATCH 06/10] wip --- transpiler/OPTIMIZATION_PLAN.md | 334 -------------------------------- transpiler/codegen/generator.py | 90 +-------- transpiler/sol2ts.py | 14 +- 3 files changed, 5 insertions(+), 433 deletions(-) delete mode 100644 transpiler/OPTIMIZATION_PLAN.md diff --git a/transpiler/OPTIMIZATION_PLAN.md b/transpiler/OPTIMIZATION_PLAN.md deleted file mode 100644 index ba8f9dfd..00000000 --- a/transpiler/OPTIMIZATION_PLAN.md +++ /dev/null @@ -1,334 +0,0 @@ -# extruder Optimization Plan - -This plan tracks larger internal cleanups for making `transpiler/` easier to -ship as a standalone package while keeping the public API stable. - -Public API to preserve: - -- CLI shape: `python3 -m transpiler ...` -- `SolidityToTypeScriptTranspiler(...)` -- `transpile_file()`, `transpile_directory()`, `write_output()`, - `discover_types()`, `emit_replacement_stub()` -- `TypeScriptCodeGenerator` imports and compatibility shim methods -- Generated TypeScript class/runtime API - -## Phase 0: Current Cleanup Baseline - -Status: in progress on the current branch. - -Done: - -- Document module-based execution and remove direct-script instructions. -- Remove `--metadata-only`. -- Remove duplicate directory discovery call in CLI directory mode. -- Avoid rediscovering AST types for files already covered by discovery roots. -- Route primitive cast call lowering through `TypeConverter`. -- Generate best-effort Solidity-style `delete` default assignments. -- Fix stale docs around W002/W004, FAQ, enum output, runtime replacements, and - `delete` behavior. -- Fix bundled `transpiler-config.json` strict JSON syntax. - -Completed follow-up checks: - -- Added focused regression tests for `delete` default assignment. -- Added focused regression tests for config loading and `--overrides` behavior. -- Runtime replacements now win over `skipFiles`/`skipDirs`; this is documented - and covered by a regression test. - -## Phase 1: Parse Once, Reuse ASTs - -Status: initial implementation complete. - -Goal: stop lexing/parsing the same Solidity files in separate type-discovery, -metadata, and generation paths. - -Keep public API by introducing an internal session object, for example -`TranspileSession` or `ProjectCompilation`, owned by -`SolidityToTypeScriptTranspiler`. - -Internal responsibilities: - -- Discover source files once. -- Read file contents once. -- Parse each file into an AST once. -- Cache ASTs by resolved path. -- Build `TypeRegistry` from cached ASTs. -- Feed cached ASTs into codegen and metadata extraction. - -Likely steps: - -1. Add a private source-file collection helper that applies `skipFiles` and - `skipDirs`. -2. Add a private `_parse_file_cached(path)` helper. **Done.** -3. Change discovery to build the registry from cached ASTs when possible. - **Done.** -4. Change `transpile_directory()` to use the same cached ASTs. **Done.** -5. Keep `transpile_file()` behavior unchanged for callers that invoke it - directly. - -Current notes: - -- `SolidityToTypeScriptTranspiler` now owns an `_ast_cache`. -- Type discovery through the transpiler uses cached ASTs instead of - `TypeRegistry.discover_from_directory()`. -- `transpile_file()` reuses cached ASTs and remains callable directly. -- Regression coverage verifies directory transpilation parses each file once. -- A source-file collection helper is still worth adding before or during the - config-loader phase so skip/replacement precedence has one path. - -Success criteria: - -- Existing tests pass. -- Direct `transpile_file()` still works without a prior directory scan. -- Directory transpilation parses each included file once in the normal path. - -## Phase 2: Single Config Loader - -Status: initial implementation complete. - -Goal: make config behavior explicit and centralized. - -Create a `TranspilerConfig` dataclass/module that loads and normalizes: - -- `runtimeReplacements` -- runtime replacement classes/mixins/methods -- `dependencyOverrides` -- `interfaceAliases` -- `skipFiles` -- `skipDirs` - -It should answer questions like: - -- `should_skip_file(rel_path) -> bool` -- `should_skip_dir(rel_path) -> bool` -- `runtime_replacement_for(rel_path) -> Optional[Replacement]` -- `dependency_override(contract, param) -> ...` -- `interface_alias(name) -> ...` - -Likely steps: - -1. Move strict JSON loading and validation out of `sol2ts.py`. **Done.** -2. Reuse the same loader in `DependencyResolver`. **Done.** -3. Reuse the same loader in `init.py` merge/read paths. **Partially done: - config reads now use the shared loader; write/merge logic remains in - `init.py`.** -4. Normalize paths once, using POSIX-style relative paths. **Done for loader - consumers.** -5. Make replacement-vs-skip precedence explicit. - -Current notes: - -- Added `transpiler/config.py` with `TranspilerConfig`. -- `sol2ts.py` keeps its historical public attributes while sourcing them from - `TranspilerConfig`. -- `DependencyResolver` now loads dependency overrides and interface aliases - through the shared loader, including legacy top-level `overrides`. -- `init.py` config reads and non-destructive write merges now go through the - shared config module. -- Runtime replacements now take precedence over `skipFiles`/`skipDirs` and - avoid parsing the replaced Solidity file. -- Added focused config-loader tests for normalization, resolver integration, - config merges, `--overrides` skip behavior, and replacement precedence. - -Remaining work: - -- Consider exporting structured replacement entries instead of raw dicts once - call sites are smaller. - -Success criteria: - -- No duplicated config parsing logic. -- Invalid config produces one clear warning/error path. -- `--overrides` semantics are no longer split between codegen and factories. - -## Phase 3: Type Service - -Status: complete. - -Goal: consolidate type reasoning that was spread across generators. - -`TypeConverter` is the single class for Solidity-to-TypeScript type -conversion, defaults, type-cast emission, and the higher-level semantic -decisions (expression type resolution, mapping/index handling, ABI type -mapping) that the generators rely on. - -Success criteria: - -- Generated output stays behaviorally equivalent. -- `delete`, cast, mapping, and ABI behavior all use one source of type truth. -- Generator classes lose type-analysis helper code. - -Current notes: - -- `TypeConverter` owns conversion, defaults, semantic access resolution, - delete/mapping defaults, index conversion, and ABI type mapping. There is no - separate `TypeService` class; the earlier inheritance shim has been removed - and its methods absorbed into `TypeConverter`. -- `ExpressionGenerator` and `StatementGenerator` delegate type-driven - decisions to the converter instead of carrying local helper implementations. -- `BaseGenerator` only owns indentation, qualified-name lookup, and padding - formatters. -- `AbiTypeInferer` accepts an optional `type_converter` for shared - Solidity-to-ABI string mapping while remaining usable standalone. -- `replacement_stub.py` instantiates `TypeConverter` directly for stub - scaffolding. - -## Phase 4: Shared AST Visitor - -Goal: reduce repeated AST traversal code. - -Registry discovery, metadata extraction, diagnostics, init red-flag scanning, -and future lint passes all walk similar AST shapes. Add a small visitor utility -that supports: - -- SourceUnit -- ContractDefinition -- FunctionDefinition -- Statement trees -- Expression trees - -Likely steps: - -1. Add a minimal visitor in `parser/visitor.py` or `analysis/visitor.py`. -2. Port diagnostics scanning first; it is small and low risk. -3. Port `MetadataExtractor`. -4. Port init red-flag/MAYBE scanning. -5. Consider porting `TypeRegistry.discover_from_ast` last. - -Success criteria: - -- Traversal logic is shared. -- Each analysis pass contains only its own decisions. -- No AST node API changes required. - -Current notes: - -- Added `parser/visitor.py` with `ASTVisitor` and `iter_child_nodes`. -- Added traversal regression coverage for nested statements and expressions. -- Ported transpiler diagnostics to `AstDiagnosticVisitor`. -- Ported `MetadataExtractor` to `ASTVisitor` dispatch. -- Ported `extruder init` red-flag scanning to a visitor instead of a local - child-iterator implementation. - -Remaining work: - -- Consider porting `TypeRegistry.discover_from_ast` after a real-world - transpile comparison; it is broader and currently stable. - -## Phase 5: Lowering Layer - -Goal: simplify TypeScript emission by normalizing Solidity semantic constructs -before codegen. - -This is the largest change and should wait until parse caching, config, and -type service are stable. - -Candidate lowerings: - -- `delete target` -> typed default assignment when resolvable. -- `require` / `assert` / `revert` -> explicit throw nodes. -- Primitive casts -> normalized cast nodes. -- Interface/address casts -> registry lookup nodes. -- Low-level calls -> explicit placeholder/fallback nodes. -- Mapping default reads and nested mapping initialization. - -Public API stays the same; this is an internal AST-to-lowered-AST step between -parse and codegen. - -Success criteria: - -- Codegen modules become mostly straightforward TypeScript string emission. -- Unsupported/degraded semantics are easier to diagnose before emission. -- The lowering step can be tested independently on small AST snippets. - -Current notes: - -- Added `lowering.py` with an in-place AST transformer. -- `TypeScriptCodeGenerator.generate()` runs the lowering pass before emission. -- Primitive cast-style function calls such as `uint256(x)` and `address(x)` - lower to explicit `TypeCast` nodes. The fallback path in - `_handle_type_cast_call` was removed: primitive casts are *only* handled via - the `TypeCast` node path, so the lowering pass is now load-bearing rather - than opportunistic. -- Added focused lowering-layer regression coverage. - -Deferred candidates (require IR work, not yet started): - -- `delete` and mapping-default lowerings need typed semantic context; they - remain in `TypeConverter` for now. -- `require` / `assert` / low-level call lowerings need dedicated lowered AST - nodes or a clearer statement IR before they can be moved off the codegen - path. -- Interface/address casts and registry lookup nodes likewise. - -These deferred items are the bulk of what Phase 5 originally promised; only -primitive cast normalization has actually shipped. Picking them up requires -introducing new AST node types (e.g. `Throw`, `InterfaceCast`, -`MappingRead`) so the lowering pass has somewhere to lower *to*. - -## Phase 6: Standalone Packaging - -Goal: make `extruder` installable without changing current module execution. - -Likely steps: - -1. Add a package-specific `pyproject.toml` or root package metadata for - `extruder`. -2. Add a console script entry point: - - ```toml - [project.scripts] - extruder = "transpiler.sol2ts:main" - ``` - -3. Ensure package data includes: - - `runtime/*.ts` - - `docs/*.md` - - default `transpiler-config.json` -4. Keep `python3 -m transpiler` working. -5. Update docs to say both `extruder ...` and `python3 -m transpiler ...` are - supported once the console script exists. - -Success criteria: - -- Fresh checkout can run module CLI. -- Installed package can run `extruder`. -- Runtime files are present in built distributions. - -Current notes: - -- Added `transpiler/pyproject.toml` for the standalone `extruder` package - without changing the root project metadata. -- Declared the `extruder = "transpiler.sol2ts:main"` console script. -- Included runtime TS files, docs, `transpiler-config.json`, and `tsconfig.json` - as package data. -- Updated README and quickstart docs to show both installed `extruder ...` and - source-checkout `python3 -m transpiler ...` usage. -- Added packaging metadata regression coverage. - -## Implementation Order - -Recommended order: - -1. Phase 1: Parse once, reuse ASTs. -2. Phase 2: Single config loader. -3. Phase 3: Type service. -4. Phase 4: Shared AST visitor. -5. Phase 6: Standalone packaging. -6. Phase 5: Lowering layer. - -The lowering layer is deliberately last: it has the biggest upside, but it is -easier and safer after type/config/traversal logic has one home. - -## Test Strategy - -Keep the current Python suite as the fast baseline, then add focused tests as -each phase lands: - -- Parse-cache tests: count or instrument parse calls for directory transpile. -- Config-loader tests: valid config, invalid JSON, path normalization, - replacement-vs-skip precedence. -- Type-service tests: defaults, casts, array indexes, mapping indexes, delete. -- Visitor tests: traversal hits nested statements/expressions exactly once. -- Packaging tests: `python3 -m transpiler --help`, console script help once - packaging exists, and runtime package-data presence. diff --git a/transpiler/codegen/generator.py b/transpiler/codegen/generator.py index fe8e9446..a6680a21 100644 --- a/transpiler/codegen/generator.py +++ b/transpiler/codegen/generator.py @@ -6,7 +6,7 @@ specialized generators for different AST node types. """ -from typing import Optional, Set, Dict, List +from typing import Optional, Set, Dict from .context import CodeGenerationContext from .type_converter import TypeConverter @@ -154,94 +154,6 @@ def generate(self, ast: SourceUnit) -> str: return '\n'.join(output) - # ========================================================================= - # COMPATIBILITY SHIMS - # ========================================================================= - # Public surface preserved for callers that hold a TypeScriptCodeGenerator - # directly. New code should use the underlying ctx/specialized generators. - - @property - def indent_level(self) -> int: - return self._ctx.indent_level - - @indent_level.setter - def indent_level(self, value: int): - self._ctx.indent_level = value - - @property - def indent_str(self) -> str: - return self._ctx.indent_str - - def indent(self) -> str: - return self._ctx.indent() - - @property - def current_state_vars(self) -> Set[str]: - return self._ctx.current_state_vars - - @current_state_vars.setter - def current_state_vars(self, value: Set[str]): - self._ctx.current_state_vars = value - - @property - def current_methods(self) -> Set[str]: - return self._ctx.current_methods - - @current_methods.setter - def current_methods(self, value: Set[str]): - self._ctx.current_methods = value - - @property - def var_types(self) -> Dict: - return self._ctx.var_types - - @var_types.setter - def var_types(self, value: Dict): - self._ctx.var_types = value - - @property - def known_structs(self) -> Set[str]: - return self._ctx.known_structs - - @property - def known_enums(self) -> Set[str]: - return self._ctx.known_enums - - @property - def known_contracts(self) -> Set[str]: - return self._ctx.known_contracts - - @property - def known_interfaces(self) -> Set[str]: - return self._ctx.known_interfaces - - def get_qualified_name(self, name: str) -> str: - return self._ctx.get_qualified_name(name) - - def generate_expression(self, expr) -> str: - return self._expr_generator.generate(expr) - - def generate_statement(self, stmt) -> str: - return self._stmt_generator.generate(stmt) - - def generate_function(self, func) -> str: - return self._func_generator.generate_function(func) - - def generate_struct(self, struct) -> str: - return self._def_generator.generate_struct(struct) - - def generate_enum(self, enum) -> str: - return self._def_generator.generate_enum(enum) - - def generate_contract(self, contract) -> str: - return self._contract_generator.generate_contract(contract) - - def solidity_type_to_ts(self, type_name) -> str: - return self._type_converter.solidity_type_to_ts(type_name) - - def default_value(self, ts_type: str) -> str: - return self._type_converter.default_value(ts_type) - # ========================================================================= # PRIVATE METHODS # ========================================================================= diff --git a/transpiler/sol2ts.py b/transpiler/sol2ts.py index 81330bbc..de1f92d8 100644 --- a/transpiler/sol2ts.py +++ b/transpiler/sol2ts.py @@ -28,7 +28,7 @@ import shutil from pathlib import Path -from typing import Optional, List, Dict, Set +from typing import Optional, List, Dict # Import from refactored modules from .lexer import Lexer @@ -70,12 +70,6 @@ def __init__( # Load consolidated transpiler configuration config_path = self.overrides_path or TranspilerConfig.default_path() self.config = TranspilerConfig.load(config_path, warn_missing=True) - self.runtime_replacements: Dict[str, dict] = self.config.runtime_replacements - self.runtime_replacement_classes: Set[str] = self.config.runtime_replacement_classes - self.runtime_replacement_mixins: Dict[str, str] = self.config.runtime_replacement_mixins - self.runtime_replacement_methods: Dict[str, Set[str]] = self.config.runtime_replacement_methods - self.skip_files: Set[str] = self.config.skip_files - self.skip_dirs: Set[str] = self.config.skip_dirs # Run type discovery on specified directories if discovery_dirs: @@ -187,9 +181,9 @@ def transpile_file(self, filepath: str, use_registry: bool = True) -> str: self.registry if use_registry else None, file_depth=file_depth, current_file_path=current_file_path, - runtime_replacement_classes=self.runtime_replacement_classes, - runtime_replacement_mixins=self.runtime_replacement_mixins, - runtime_replacement_methods=self.runtime_replacement_methods, + runtime_replacement_classes=self.config.runtime_replacement_classes, + runtime_replacement_mixins=self.config.runtime_replacement_mixins, + runtime_replacement_methods=self.config.runtime_replacement_methods, ) return generator.generate(ast) From 490cbc4513e473fe056129fd2365ed0eab013e80 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Tue, 28 Apr 2026 12:24:55 -0700 Subject: [PATCH 07/10] clean up transpiler --- transpiler/codegen/expression.py | 22 ++++++--- transpiler/codegen/generator.py | 2 - transpiler/lowering.py | 83 -------------------------------- transpiler/test_transpiler.py | 28 ----------- 4 files changed, 16 insertions(+), 119 deletions(-) delete mode 100644 transpiler/lowering.py diff --git a/transpiler/codegen/expression.py b/transpiler/codegen/expression.py index ec89a0a8..636f3240 100644 --- a/transpiler/codegen/expression.py +++ b/transpiler/codegen/expression.py @@ -547,12 +547,13 @@ def _handle_special_function(self, call: FunctionCall, name: str, args: str) -> return None def _handle_type_cast_call(self, call: FunctionCall, name: str, args: str) -> Optional[str]: - """Handle non-primitive cast-like calls: interface casts, struct constructors, enum coercions. - - Primitive casts (``uint256(x)``, ``address(x)`` etc.) are normalized to ``TypeCast`` - nodes by the lowering pass and never reach this handler. - """ - if name.startswith('I') and len(name) > 1 and name[1].isupper(): + """Handle type cast function calls (uint256(x), address(x), etc.).""" + if self._is_primitive_cast_name(name): + if len(call.arguments) != 1: + return args + cast = TypeCast(type_name=TypeName(name=name), expression=call.arguments[0]) + return self._type_converter.generate_type_cast(cast, self.generate) + elif name.startswith('I') and len(name) > 1 and name[1].isupper(): # Interface cast return self._handle_interface_cast(call, args) elif name[0].isupper() and call.named_arguments: @@ -577,6 +578,15 @@ def _handle_type_cast_call(self, call: FunctionCall, name: str, args: str) -> Op return None + @staticmethod + def _is_primitive_cast_name(name: str) -> bool: + return ( + name in ('address', 'bool', 'bytes', 'bytes32', 'payable', 'string') + or name.startswith('uint') + or name.startswith('int') + or (name.startswith('bytes') and name[5:].isdigit()) + ) + def _handle_interface_cast(self, call: FunctionCall, args: str) -> str: """Handle interface type cast like IEffect(address(x)). diff --git a/transpiler/codegen/generator.py b/transpiler/codegen/generator.py index a6680a21..33d2f497 100644 --- a/transpiler/codegen/generator.py +++ b/transpiler/codegen/generator.py @@ -16,7 +16,6 @@ from .definition import DefinitionGenerator from .imports import ImportGenerator from .contract import ContractGenerator -from ..lowering import lower_ast from ..parser.ast_nodes import SourceUnit from ..type_system import TypeRegistry @@ -104,7 +103,6 @@ def generate(self, ast: SourceUnit) -> str: Returns: The generated TypeScript code as a string """ - ast = lower_ast(ast) output = [] # Reset context for this file diff --git a/transpiler/lowering.py b/transpiler/lowering.py deleted file mode 100644 index 88eff9de..00000000 --- a/transpiler/lowering.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Internal AST lowering before TypeScript code generation. - -Lowering normalizes Solidity constructs into a smaller set of AST shapes while -preserving the public parser/codegen APIs. The first pass is intentionally -small: primitive cast-style function calls become explicit ``TypeCast`` nodes. -""" - -from dataclasses import fields, is_dataclass - -from .parser.ast_nodes import ( - ASTNode, - FunctionCall, - Identifier, - TypeCast, - TypeName, -) - - -PRIMITIVE_CAST_NAMES = { - 'address', - 'bool', - 'bytes', - 'bytes32', - 'payable', - 'string', -} - - -def lower_ast(ast: ASTNode) -> ASTNode: - """Lower an AST in place and return it.""" - return SolidityLowerer().visit(ast) - - -def is_primitive_cast_name(name: str) -> bool: - return ( - name in PRIMITIVE_CAST_NAMES - or name.startswith('uint') - or name.startswith('int') - or (name.startswith('bytes') and name[5:].isdigit()) - ) - - -class SolidityLowerer: - """Small in-place AST transformer.""" - - def visit(self, node): - if node is None: - return None - if isinstance(node, ASTNode): - method = getattr(self, f'visit_{type(node).__name__}', self.generic_visit) - return method(node) - return node - - def generic_visit(self, node: ASTNode): - if not is_dataclass(node): - return node - for field in fields(node): - setattr(node, field.name, self._lower_value(getattr(node, field.name))) - return node - - def _lower_value(self, value): - if isinstance(value, ASTNode): - return self.visit(value) - if isinstance(value, list): - return [self._lower_value(item) for item in value] - if isinstance(value, tuple): - return tuple(self._lower_value(item) for item in value) - if isinstance(value, dict): - return {key: self._lower_value(item) for key, item in value.items()} - return value - - def visit_FunctionCall(self, node: FunctionCall): - self.generic_visit(node) - if not isinstance(node.function, Identifier): - return node - if node.named_arguments or node.call_options: - return node - if len(node.arguments) != 1: - return node - name = node.function.name - if not is_primitive_cast_name(name): - return node - return TypeCast(type_name=TypeName(name=name), expression=node.arguments[0]) diff --git a/transpiler/test_transpiler.py b/transpiler/test_transpiler.py index 3e4866c1..95cdde6a 100644 --- a/transpiler/test_transpiler.py +++ b/transpiler/test_transpiler.py @@ -1078,34 +1078,6 @@ def test_address_cast(self): self.assertIn('getAddr', output) -class TestLoweringLayer(unittest.TestCase): - """Test pre-codegen AST lowering.""" - - def test_primitive_cast_call_lowers_to_type_cast_node(self): - from transpiler.lowering import lower_ast - from transpiler.parser.ast_nodes import ReturnStatement, TypeCast - - source = ''' - contract TestContract { - function cast(int256 x) public pure returns (uint256) { - return uint256(x); - } - } - ''' - - lexer = Lexer(source) - tokens = lexer.tokenize() - parser = Parser(tokens) - ast = parser.parse() - - lowered = lower_ast(ast) - stmt = lowered.contracts[0].functions[0].body.statements[0] - - self.assertIsInstance(stmt, ReturnStatement) - self.assertIsInstance(stmt.expression, TypeCast) - self.assertEqual(stmt.expression.type_name.name, 'uint256') - - class TestDeleteGeneration(unittest.TestCase): """Test Solidity delete semantics in generated TypeScript.""" From f95cfdb9f16e5fefb1eff5526efbf17d13ba57b8 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Thu, 30 Apr 2026 17:44:27 -0700 Subject: [PATCH 08/10] wip damage gifs --- drool/imgs/aurox_front_damage.gif | Bin 0 -> 6360 bytes drool/imgs/ekineki_front_damage.gif | Bin 0 -> 5751 bytes drool/imgs/embursa_front_damage.gif | Bin 0 -> 8175 bytes drool/imgs/ghouliath_front_damage.gif | Bin 0 -> 6888 bytes drool/imgs/gorillax_front_damage.gif | Bin 0 -> 8148 bytes drool/imgs/iblivion_front_damage.gif | Bin 0 -> 6099 bytes drool/imgs/inutia_front_damage.gif | Bin 0 -> 6442 bytes drool/imgs/malalien_front_damage.gif | Bin 0 -> 5856 bytes drool/imgs/mon_spritesheet.png | Bin 46806 -> 64673 bytes drool/imgs/mon_switch.png | Bin 48113 -> 45811 bytes drool/imgs/pengym_front_damage.gif | Bin 0 -> 6576 bytes drool/imgs/sofabbi_front_damage.gif | Bin 0 -> 5990 bytes drool/imgs/spritesheet.json | 180 +++++++++++++++----------- drool/imgs/volthare_front_damage.gif | Bin 0 -> 6676 bytes drool/imgs/xmon_front_damage.gif | Bin 0 -> 7348 bytes processing/buildDamageGifs.py | 86 ++++++++++++ processing/createMonSpritesheets.py | 35 ++++- processing/generateMonsTypeScript.py | 3 + transpiler/MIGRATION.md | 112 ---------------- 19 files changed, 225 insertions(+), 191 deletions(-) create mode 100644 drool/imgs/aurox_front_damage.gif create mode 100644 drool/imgs/ekineki_front_damage.gif create mode 100644 drool/imgs/embursa_front_damage.gif create mode 100644 drool/imgs/ghouliath_front_damage.gif create mode 100644 drool/imgs/gorillax_front_damage.gif create mode 100644 drool/imgs/iblivion_front_damage.gif create mode 100644 drool/imgs/inutia_front_damage.gif create mode 100644 drool/imgs/malalien_front_damage.gif create mode 100644 drool/imgs/pengym_front_damage.gif create mode 100644 drool/imgs/sofabbi_front_damage.gif create mode 100644 drool/imgs/volthare_front_damage.gif create mode 100644 drool/imgs/xmon_front_damage.gif create mode 100644 processing/buildDamageGifs.py delete mode 100644 transpiler/MIGRATION.md diff --git a/drool/imgs/aurox_front_damage.gif b/drool/imgs/aurox_front_damage.gif new file mode 100644 index 0000000000000000000000000000000000000000..e745a3406303356032c13e6ae67e441343f75d05 GIT binary patch literal 6360 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 literal 0 HcmV?d00001 diff --git a/drool/imgs/embursa_front_damage.gif b/drool/imgs/embursa_front_damage.gif new file mode 100644 index 0000000000000000000000000000000000000000..c6a6d061dfcbe89efec42d7fd7464126bd1c0d24 GIT binary patch literal 8175 zcmeI0XEdDcy2q#Jz1Jby=sgl8i44JvkPw5>4KaF;E*QNf%3#!pC@}~TodlDJUV`XB zbRt9xA|$r=J?E^w&suw(?`N;vAD(AD_jX&g~EN5XB%(CJ|9Zj`Fx z=2^BZVK`rdT3{7X0MjeFnMoxD^K~=;7?5&Jad}k$U)*^@SJ!e-HUV&KS5d%TC8Ekq z`#0?*3aUQE_-vO$IWp}{a>+KS==jjj0PiA?b6S69!q9L1_;r#bO}CK*@8G+_vkjLW z-ls?*eG@p>KX&`wsBq`z+S({D36yrfu0+Is0?#b94YpK;=fBiG)(qrS+9Epo9~cjD zS7ICA+Q{I4%`}-bG-SJYgetmv?i81WogaTwIypLU3ybtuQ7t*#yyH+aCuO3%GUsFo zkJWqF>O)+k<>PcuTf|%Wo<@tLg=GR|Kf95tbJ)1MKek6k*roa`29#n%3hXFpQiygS z4wGk|r?euYF}u-+3vTAhXc&lMI16ho&xQRQFL}gaj*$GeUJ%|@l5_}p5BQ^@SNVc@J_~-1x!#7n+Qn9eVh>gi=0&Y;Ul8|y`{@~5EpxS8~G z-QY_{Q`Ib?U6_P>nmJsx-s=5k4tn(tGtsJSr!JNDRnAC2>TULnf@P_S9CUg^7QgPo zb4O;od^NJFbX?6CM@%2iB-6`azPZYW%}lf!5ZE^9k*?=wZdB^Gs_Yo4ZP{b2=XRsS ztfuqX6`uR6m0hnK_+Q3+vqIUFEGM-nmtsMkRSg&$scQ%a?>Mn);=m+6NO%a9fe?uX z{g5PrsD%cxO+aI99|NtA*>xZ8SPv3EuDx}#9$8-%E*RCNuC8ygp{6(*vJGp3fiaW1 zi-E{*$r&wPk86f4$S?F6PnL{#{@Z zRAF(p0Veru@U7*v%G;wJRrcog!13?1gEzqenY)q5hrbR-mKA?#cRcuge7qyzvG4<$ z@xgL?V?D9qyR_WrS>UcYUEA!n8a*74oB{?Qox+0`WWx5C2LPxCfb)0~EE!D!kW>^4 zw*R0!g)AT$sZj%~FbDxi(3W&@H>p+VBlmM3k)RxjRH3_qR{V<;A-kB(&HTAYBEgP=daL-vP z_q=Rvj4Fxpn1cQOW?>)MCJsOR1|eNW*=w#koLahNr_{UA>oa4J#&QCcye9A=a81$y z2>fR~WCqXzQ~(BlYr};!To}WJH(W@=MVY+thX0a=3sJc6h6`!9P=^a|_^X8eS$`7v z&r0B5eSnb(@wX3vC?%s1Ug+>dR-i??lH76!2&m3hKp|hOkxX?h0z#B0B4Q;>^EFIU ziZt;`U>ZjySg9_UOl9ps*E{~L>J9wpr z51l^7YnhWV5RC!j4X%|nlAcFm;Vo9!>RfWIWRH7nak9TUBp7-g>keC<*`lnX*XM(U z`)wt*!*BjD_y2m}8ZV-c#kPZfk$#8r1bokCvqX(`l9~f36sD{J)zBEL@@);HMGGIW$fM@tT}uc87$X~k`5n16W2R6y-wnQ` z%y56cWOnIe!4!yiia??f(D&t>b(q5b25Mxt5*T3@&6`q1+dA3s=>mgg3*}A3Ay%(Y5)6D3B z=wpWhrjSFMGUXE}WA!x^XKQ`!Xedfg>etZ{+JQ%OJl|A}oB$teMqmwAj(!wY(?m;1 zH!IO{@H|+Un1sMhpyNdzwi8qY9};W^^3nWHPLKA}BMCRa8QT`q9gKnIPClJZl&p;n zXEZJ0_p>aQ$!Tsks@gtIXZhvRId5jRXru@3{J2SU1GuMUBx@Bm7*y#@#Cb z@MZruUVVzEB*nJUH%V$UZ%`^8cwP-YD{~aQ<>ceX-634pj{I=sJydM@>+&EESn4p@ z5b0wwrA0ps7*+=l!oW9MdgYyD)W#G(MQomrU3^!Z`DdGagTm%7EtV2qx`Xfd*9g#3 zu`kI2o15Ark=!LK_Tk zvkv9jvehhDce+aM5h^=hRvTiv*Y!E+;Z`4f=*<_oBD&Vh(M!4^)^Ee;{K}?L11AUU z2T;|a{?C@q$Fe-KA$z1v)^BoeS+2C5{UG-w;(WWp+JEz(Tsr0OX2j3#V1Hi2f^;G( ze$c|gsdNi|&ngfjSoFonWeb4Is7AcfZs^g+1q*Re2IE%}k@REZb>%oUmYT!}S<~q{ ztsa$n{S*t^w{g3CzSYGR`erN&HuOE~0@uNP^l|t1#_FUgl&eK|Wk}!l0!a#Bx`X0R0a!Me(L5L_$Ffn-53Srm_qv`+Y1-<0x2R@ z*1;t1ozD1TdHrWhA5tJG!p*6qN?d?ooR9#0Jo}VGMhwox`#zK~dtW%5vqzljfrMwB zot#dYw*HW}#GsG87#Q?VxHAE$0oMVB{{#1bI}!K;_x~)8{W;CQN#I}M&dkJ@@^`aO zE^Wtg(d@@C%Q(-9rV4Z?Gm1!)Xfp|hgGjIA6&wpzJMm;}&n*InVc1+U@tOC^ID55R zPy|`e2?JsHkwhvNMV|r|5DUt?dIeh}()&m!n`*r8P!v^eWE2X?HmdlT>o}rC3tMpM z%T^`DTjn;nn^h|7C9qJ}Yz+dj3tHX$ow_0 z^_36dBL+Cg%EsvHSY6KUS0D8#|!g|!A9rv zbHRu1YcIi3P^&lS`$Ht2^b|P-P|x*wOX_o86-cvDB*7(dtgZDmaGr`xbeKm~DRBA= zyZ?(DA^fMn56}Vt2rSM}06YrE@Op2g+UJJ>Ng6LOZ-_tx1~h-#AXQVg_a)e>kqP11j*AXTK>;(dbo&=@hl<0N zA8Q^>T1Va}tP`?fow0+Z9cd~v`%&zfe8O?rmz4lnXW|OY&^Dz{y|kr`CIPGyMgkar z)2o(F4DnwCEyGIU@oC^C)69r0BX;3r(-KXQCseM=Pt#&XF?Nacax;bnA|O0#i7053 zLo-1oYQ`ecNC~F1KTR;U^t3!@EizOmy)8<}0E*8MN^(>rDqCit8J0brK_iJpAnq47 z_HQR_UMB8~+M;}YSu=D`Y-UQIiix>Y0t5tHa&7O2o3NTwhxBI$)n>R<-B0k1o_U@7 zn3XNmh&HpLNxLnJ2Oi(4!Q|l&&=$j`z~%mLb9_X_;kh?e4mbK~kONh}~vOSj4_ zoAO$4ln+)lfq|m^89*zkY-9RE+(uEFv?P*{OlY}V)A%hYKkySPWP`T1Nq9N8Ei|hd zwm%=dtt2hss`K8%kukWRIV|AdS@ZYn`#7ECv})N8p}$6Boo})n$9t@t?kwx<1Ri(6 zzZX6G)SlBiuXN;#J$jYs*Zv)Uh#~$urA7nju!;~K02cg)67&g`{sOQY4!+I@e1L%g z5eyOJ8=rO7-lfx+$%>NXp{pkLZ&K4$LwUa!9L^p~RTeV{50vYW-S|<-s#1Ip-I4eu z-B^Wgen_6iwRd|g%a;QZK`U&*&%4F6MEpfb@0ZH`j3F1336_4v#{(e(7vUO0X8nn` zNCwz0H?c&R?1q_-EwMc?(YVwg7vPjyz(H@K9#P==h+X3n%UcQ!8LF=l76m>B)sO{jGE^8B$&`J2q4Wv{1e<;& zV_489n$_!d7}q1lC1>i)_BIDACSj(vH@_sbCi=U5?1-If>-?IDH6=cf%i-xsskVKe z#dx{lgyV}z>=iLwlmFB~asxbO6NLHZNc18vLIWai*%&By=@zzyBe(;aYV7+cs ze(Mka-2b1odEVLT?AMpb0W z(^@4?5J^_uE0A~BnFaG?d@g?du}FR_r$>kxW)jn|7{`h0d$ne!rH)%QcXM9KBHFm+ za5!bB8_%rX_s6IZ;{(m6?29vb5aU`txN~E!q|&*1ap%{2CxYk%Gdx$Nx={eeHdgZs zM9P-JrKJeC&fBznR*s?G6n}E}x{6 zZ_TDJ2u5j_&U^MRmcL#J>=0B~EnS-V9o#?AZ*|x*ntb*%W{_QDc_0S!vUNzy@8<|dSo~|^k4-}t(I6Yx!ceX#bpB*q;o1OVrf*16{$PwtXBaxeczD@lZ zZa(m7cRNXrUf9omDgZadbqd}DD;gRb5GfiK&4P*KavfY{`&i9ZlO!uD^o(Y8^2W2_ zM5_{So^8oUb`~xMslg8P*Jaf#HttYaxvR7NbkSKULKDey%GV9=JtVIw9Mg{^$;+og zHEOP6?4#ar&I8C(U>k?-2@ffYURGgp s4X*J{O?YpOT2azbv)_Ff{;2p0T>Xw?%Sc2^WC`@1z>pWy#Vx(|4_ys|y8r+H literal 0 HcmV?d00001 diff --git a/drool/imgs/ghouliath_front_damage.gif b/drool/imgs/ghouliath_front_damage.gif new file mode 100644 index 0000000000000000000000000000000000000000..0f52a7139a7f9eafcdab9db75c36b578014c4d2f GIT binary patch literal 6888 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 D**8Ys literal 0 HcmV?d00001 diff --git a/drool/imgs/gorillax_front_damage.gif b/drool/imgs/gorillax_front_damage.gif new file mode 100644 index 0000000000000000000000000000000000000000..965664f5b1f9927088407c92d90ea683c00a6df2 GIT binary patch literal 8148 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 E0R;@Py#N3J literal 0 HcmV?d00001 diff --git a/drool/imgs/iblivion_front_damage.gif b/drool/imgs/iblivion_front_damage.gif new file mode 100644 index 0000000000000000000000000000000000000000..fd8191a18e9e93e2b2d5428535f4028fcc0316a5 GIT binary patch literal 6099 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 literal 0 HcmV?d00001 diff --git a/drool/imgs/inutia_front_damage.gif b/drool/imgs/inutia_front_damage.gif new file mode 100644 index 0000000000000000000000000000000000000000..f7baa662ce91a9a32b8c1a5e276f53165cfe9341 GIT binary patch literal 6442 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

u71QXFLS~54JXE3DKt*-vP7CelIms@vK=` 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 literal 46806 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 N`_|pgFB~b%{{yGJEldCa diff --git a/drool/imgs/mon_switch.png b/drool/imgs/mon_switch.png index fef9edaf56423cf139c8e6eb6a7cd960661fb2aa..18e3cc3bb248665dd151953e93765c9feaef4e34 100644 GIT binary patch literal 45811 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 T&i@3z00000NkvXXu0mjfOkNTg literal 48113 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 literal 0 HcmV?d00001 diff --git a/drool/imgs/sofabbi_front_damage.gif b/drool/imgs/sofabbi_front_damage.gif new file mode 100644 index 0000000000000000000000000000000000000000..e18d25ecb3fe65a0676d12f97728e2be65a4f08d GIT binary patch literal 5990 zcmeI0=Tj5d8pe}Q6UtITTf!wMU0?yFiWpEJbODkz=M5m-cU)s<#}i_YBHxwHSlUEUApoH=v8yw9BH_nbLq=BB!O9#{Yta1-#O z4h{~6SZ@v&$%&+`Wa+uwz^#{<4^L`8EiAmS`VCNEqtdJL^Wf2Nq`?210x}0YHYSb^ z=Zx%3G}YAEfQS9b?6HF(hx5-%|HCE#?gIdj8-{G6Jx=BpR(9=cqCdSUaNMF8*<9A0 zE`Bm_rmwlYHyeH~4QAfL=+9GhsB!IYsd!FR_ZlcRZ>@Y;q#L$A)8AS(REkK3NLk!r z+UKzomB*OjN;V*1nW;$5s3!`DDi`ViYvC|e{N&j0&aLkz>m0e%{~bI2ZoI&E+Cqe; zEd#9cZ-h+Nl6_q}QCy>Px(kf}wp)V7yCfItUp&HB@pG!xp3q~;ZN_Z}zD^-4#4 zx8i_^dt)w4@lA^QY4h13fI$A`63h-^Occ#M$XEdKftoHwgNxCNL0sz0MRzATtS2T- zLY+cT$l6*A;bUzH6lZk|CA3v3e_i?A0)Dxm!zR7&Pw$|SGm|4MJmY^wej5?OV{e|T)-v; zA&(O>76TTOTx;I?c+}c+D-wdW?ydlHQv0fct>3mWZyL}$;S3I@&+lclC6i*B`c;Xt zLi#?KnXRUMH-JHm-ECiU%Lq(y5A%PL7FZO#X;x!d;}6ODdGDQBF&YkZ7a7yc<5Zl?~D?z_i$%A+FivGdQ@`V1R3L|tgJ z|4XxpV7Q-wW_wW2)`q*4ud9zn;Nt|}BlEEw1mc2U5)FGF4r1=Z7}_mJS=C+z0-i06rt=s;Lk>+!?_d9F!W^AWs# zIrml09PlUuPv^aE0So!;Kb!|D8)o>bf6e z8*WI}?^^?lC^osBiFrkR)GU1fwWD=KE?Oo!-;K-rmiXsOyzVr>6qp z0%hl5@qHdh+XA_gODkIW=(Nw~TjX>Z3(m2OQ@*#Bl8Mz&`|J%7vpK>avul!m-5doT zG46{!5;Y{9>q$Z5ecQjE(;r2K2~^0^rNvq zXPf%NJc3?H+MwzNwf}h3)_Bj^VcEt3TtLk+>o0>5Sdi}(WzES4oi57m`4(WI}C4|bqZ$z#S7E%G&WvEbQ;_jkYtRW zJGFiaF0sy>D;|1RA|6wH0Ly1p#o^q`8H=( z=cap3pbiY-ZZ*S;g2MR0YPVR>R>1tDOG6L4fS(`&U1EjshJCU|M0jrG?sqPzqtKSv zM1rkgH#rQeguRpq+w}DSKs5U#Zg3g-m4<_uy_i_d&<=sb-R{Udf`Sdw9q19$wuG5f z5}myPAE4x#E{%!P)0g?Q?avuBF z!yzGLpIm0Bc>&du(|j}!IN)7rqEQx83J5Wuva`OTyy_UY{aQjYLJdqP1H^Yz6wnVl zk!z)R)V2fpx1Ke$zZvKHLZ(h?0k!DNUL-R!f3>45+1e0{aw;`cpoe=L)Al6^rcg}m zsf=IfUH?I zyLDlmBG&sHND^IQr6j!k1J$fiU7DX+p_UP(i(rR1pY>leB|0#Rk|k4mg9LzO^m+)E|aeeJ$ON#}ZhpnBzDT6OqZN9f30ptO zTzC>ExC2`s4jgSvqRLn5e`;|i6ZO0@oSQ3cs>{r7UfZ}>;YmfDJ#BOQPw#gG4nHqP z8eaB&mU8wS1?fV=gE(T@U1J!@o)Ms=OPFDWYYU)byp~^J$`a|Dx4DHR2Spl-`F8c z*Qg{%qQbnV2Kbd{o{(qCQq1vYIR#1tkqcHLCZYAZM0H!DIDo7XJ#Sk5@fG}@} zIxOh0#YuFJ3&oi;Vj69FyK^Ba`@64_nWqV462#l|Wrj{ID@Y^Pgg4*o0woouPCAs# zKATN#Sc*Lc3{^s309=0UJXYVasxMhBGgf^WO?sT4Uv&9FW#Q$@plZ*{Gd)ogcNe~h zG}vo8zpW|{tUw0aCs2GFa4h+~WedB8(*+O0`C?SVK0oAfx?Z0-t%%wBbOPWCybz(> q@=df&Jr$SgBNRt6v3kRVjc~JEK)7p-=fA3owDiHU)9ev|)Bgc4dxBs9 literal 0 HcmV?d00001 diff --git a/drool/imgs/spritesheet.json b/drool/imgs/spritesheet.json index a0e2e180..79f8608a 100644 --- a/drool/imgs/spritesheet.json +++ b/drool/imgs/spritesheet.json @@ -13,7 +13,7 @@ }, "Nirvamma_Front.gif": { "msPerFrame": 100, - "frames": [[768, 0], [864, 0], [960, 0], [0, 96], [96, 96], [192, 96], [288, 96], [384, 96]], + "frames": [[768, 0], [864, 0], [960, 0], [1056, 0], [0, 96], [96, 96], [192, 96], [288, 96]], "switchOut": { "msPerFrame": 100, "frames": [[768, 0], [864, 0], [960, 0], [1056, 0], [1152, 0], [1248, 0], [1344, 0], [0, 96]] @@ -23,9 +23,9 @@ "frames": [[0, 96], [1344, 0], [1248, 0], [1152, 0], [1056, 0], [960, 0], [864, 0], [768, 0]] } }, - "Nirvamma_Front_Shapes.gif": { - "msPerFrame": 100, - "frames": [[480, 96], [576, 96], [672, 96], [768, 96], [864, 96], [960, 96], [0, 192], [96, 192]], + "aurox_back.gif": { + "msPerFrame": 200, + "frames": [[384, 96], [480, 96]], "switchOut": { "msPerFrame": 100, "frames": [[96, 96], [192, 96], [288, 96], [384, 96], [480, 96], [576, 96], [672, 96], [768, 96]] @@ -35,9 +35,9 @@ "frames": [[768, 96], [672, 96], [576, 96], [480, 96], [384, 96], [288, 96], [192, 96], [96, 96]] } }, - "Nirvamma_Only_Back.gif": { + "aurox_front.gif": { "msPerFrame": 100, - "frames": [[192, 192], [288, 192], [384, 192], [480, 192]], + "frames": [[576, 96], [672, 96], [768, 96], [864, 96]], "switchOut": { "msPerFrame": 100, "frames": [[864, 96], [960, 96], [1056, 96], [1152, 96], [1248, 96], [1344, 96], [0, 192], [96, 192]] @@ -45,11 +45,15 @@ "switchIn": { "msPerFrame": 100, "frames": [[96, 192], [0, 192], [1344, 96], [1248, 96], [1152, 96], [1056, 96], [960, 96], [864, 96]] + }, + "damageFront": { + "msPerFrame": 100, + "frames": [[960, 96], [1056, 96], [0, 192], [96, 192]] } }, - "aurox_back.gif": { - "msPerFrame": 200, - "frames": [[576, 192], [672, 192]], + "ekineki_back.gif": { + "msPerFrame": 100, + "frames": [[192, 192], [288, 192], [384, 192], [480, 192]], "switchOut": { "msPerFrame": 100, "frames": [[192, 192], [288, 192], [384, 192], [480, 192], [576, 192], [672, 192], [768, 192], [864, 192]] @@ -59,9 +63,9 @@ "frames": [[864, 192], [768, 192], [672, 192], [576, 192], [480, 192], [384, 192], [288, 192], [192, 192]] } }, - "aurox_front.gif": { + "ekineki_front.gif": { "msPerFrame": 100, - "frames": [[768, 192], [864, 192], [960, 192], [0, 288]], + "frames": [[576, 192], [672, 192], [768, 192], [864, 192]], "switchOut": { "msPerFrame": 100, "frames": [[960, 192], [1056, 192], [1152, 192], [1248, 192], [1344, 192], [0, 288], [96, 288], [192, 288]] @@ -69,11 +73,15 @@ "switchIn": { "msPerFrame": 100, "frames": [[192, 288], [96, 288], [0, 288], [1344, 192], [1248, 192], [1152, 192], [1056, 192], [960, 192]] + }, + "damageFront": { + "msPerFrame": 100, + "frames": [[960, 192], [1056, 192], [0, 288], [96, 288]] } }, - "ekineki_back.gif": { - "msPerFrame": 100, - "frames": [[96, 288], [192, 288], [288, 288], [384, 288]], + "embursa_back.gif": { + "msPerFrame": 200, + "frames": [[192, 288], [288, 288]], "switchOut": { "msPerFrame": 100, "frames": [[288, 288], [384, 288], [480, 288], [576, 288], [672, 288], [768, 288], [864, 288], [960, 288]] @@ -83,9 +91,9 @@ "frames": [[960, 288], [864, 288], [768, 288], [672, 288], [576, 288], [480, 288], [384, 288], [288, 288]] } }, - "ekineki_front.gif": { - "msPerFrame": 100, - "frames": [[480, 288], [576, 288], [672, 288], [768, 288]], + "embursa_front.gif": { + "msPerFrame": 200, + "frames": [[384, 288], [480, 288], [576, 288], [672, 288]], "switchOut": { "msPerFrame": 100, "frames": [[1056, 288], [1152, 288], [1248, 288], [1344, 288], [0, 384], [96, 384], [192, 384], [288, 384]] @@ -93,11 +101,15 @@ "switchIn": { "msPerFrame": 100, "frames": [[288, 384], [192, 384], [96, 384], [0, 384], [1344, 288], [1248, 288], [1152, 288], [1056, 288]] + }, + "damageFront": { + "msPerFrame": 100, + "frames": [[768, 288], [864, 288], [960, 288], [1056, 288]] } }, - "embursa_back.gif": { + "ghouliath_back.gif": { "msPerFrame": 200, - "frames": [[864, 288], [960, 288]], + "frames": [[0, 384], [96, 384]], "switchOut": { "msPerFrame": 100, "frames": [[384, 384], [480, 384], [576, 384], [672, 384], [768, 384], [864, 384], [960, 384], [1056, 384]] @@ -107,9 +119,9 @@ "frames": [[1056, 384], [960, 384], [864, 384], [768, 384], [672, 384], [576, 384], [480, 384], [384, 384]] } }, - "embursa_front.gif": { - "msPerFrame": 200, - "frames": [[0, 384], [96, 384], [192, 384], [288, 384]], + "ghouliath_front.gif": { + "msPerFrame": 100, + "frames": [[192, 384], [288, 384], [384, 384], [480, 384]], "switchOut": { "msPerFrame": 100, "frames": [[1152, 384], [1248, 384], [1344, 384], [0, 480], [96, 480], [192, 480], [288, 480], [384, 480]] @@ -117,11 +129,15 @@ "switchIn": { "msPerFrame": 100, "frames": [[384, 480], [288, 480], [192, 480], [96, 480], [0, 480], [1344, 384], [1248, 384], [1152, 384]] + }, + "damageFront": { + "msPerFrame": 100, + "frames": [[576, 384], [672, 384], [768, 384], [864, 384]] } }, - "ghouliath_back.gif": { + "gorillax_back.gif": { "msPerFrame": 200, - "frames": [[384, 384], [480, 384]], + "frames": [[960, 384], [1056, 384]], "switchOut": { "msPerFrame": 100, "frames": [[480, 480], [576, 480], [672, 480], [768, 480], [864, 480], [960, 480], [1056, 480], [1152, 480]] @@ -131,9 +147,9 @@ "frames": [[1152, 480], [1056, 480], [960, 480], [864, 480], [768, 480], [672, 480], [576, 480], [480, 480]] } }, - "ghouliath_front.gif": { + "gorillax_front.gif": { "msPerFrame": 100, - "frames": [[576, 384], [672, 384], [768, 384], [864, 384]], + "frames": [[0, 480], [96, 480], [192, 480], [288, 480]], "switchOut": { "msPerFrame": 100, "frames": [[1248, 480], [1344, 480], [0, 576], [96, 576], [192, 576], [288, 576], [384, 576], [480, 576]] @@ -141,11 +157,15 @@ "switchIn": { "msPerFrame": 100, "frames": [[480, 576], [384, 576], [288, 576], [192, 576], [96, 576], [0, 576], [1344, 480], [1248, 480]] + }, + "damageFront": { + "msPerFrame": 100, + "frames": [[384, 480], [480, 480], [576, 480], [672, 480]] } }, - "gorillax_back.gif": { + "iblivion_back.gif": { "msPerFrame": 200, - "frames": [[960, 384], [0, 480]], + "frames": [[768, 480], [864, 480]], "switchOut": { "msPerFrame": 100, "frames": [[576, 576], [672, 576], [768, 576], [864, 576], [960, 576], [1056, 576], [1152, 576], [1248, 576]] @@ -155,9 +175,9 @@ "frames": [[1248, 576], [1152, 576], [1056, 576], [960, 576], [864, 576], [768, 576], [672, 576], [576, 576]] } }, - "gorillax_front.gif": { + "iblivion_front.gif": { "msPerFrame": 100, - "frames": [[96, 480], [192, 480], [288, 480], [384, 480]], + "frames": [[960, 480], [1056, 480], [0, 576], [96, 576]], "switchOut": { "msPerFrame": 100, "frames": [[1344, 576], [0, 672], [96, 672], [192, 672], [288, 672], [384, 672], [480, 672], [576, 672]] @@ -165,11 +185,15 @@ "switchIn": { "msPerFrame": 100, "frames": [[576, 672], [480, 672], [384, 672], [288, 672], [192, 672], [96, 672], [0, 672], [1344, 576]] + }, + "damageFront": { + "msPerFrame": 100, + "frames": [[192, 576], [288, 576], [384, 576], [480, 576]] } }, - "iblivion_back.gif": { + "inutia_back.gif": { "msPerFrame": 200, - "frames": [[480, 480], [576, 480]], + "frames": [[576, 576], [672, 576]], "switchOut": { "msPerFrame": 100, "frames": [[672, 672], [768, 672], [864, 672], [960, 672], [1056, 672], [1152, 672], [1248, 672], [1344, 672]] @@ -179,9 +203,9 @@ "frames": [[1344, 672], [1248, 672], [1152, 672], [1056, 672], [960, 672], [864, 672], [768, 672], [672, 672]] } }, - "iblivion_front.gif": { + "inutia_front.gif": { "msPerFrame": 100, - "frames": [[672, 480], [768, 480], [864, 480], [960, 480]], + "frames": [[768, 576], [864, 576], [960, 576], [1056, 576]], "switchOut": { "msPerFrame": 100, "frames": [[0, 768], [96, 768], [192, 768], [288, 768], [384, 768], [480, 768], [576, 768], [672, 768]] @@ -189,11 +213,15 @@ "switchIn": { "msPerFrame": 100, "frames": [[672, 768], [576, 768], [480, 768], [384, 768], [288, 768], [192, 768], [96, 768], [0, 768]] + }, + "damageFront": { + "msPerFrame": 100, + "frames": [[0, 672], [96, 672], [192, 672], [288, 672]] } }, - "inutia_back.gif": { + "malalien_back.gif": { "msPerFrame": 200, - "frames": [[0, 576], [96, 576]], + "frames": [[384, 672], [480, 672]], "switchOut": { "msPerFrame": 100, "frames": [[768, 768], [864, 768], [960, 768], [1056, 768], [1152, 768], [1248, 768], [1344, 768], [0, 864]] @@ -203,9 +231,9 @@ "frames": [[0, 864], [1344, 768], [1248, 768], [1152, 768], [1056, 768], [960, 768], [864, 768], [768, 768]] } }, - "inutia_front.gif": { + "malalien_front.gif": { "msPerFrame": 100, - "frames": [[192, 576], [288, 576], [384, 576], [480, 576]], + "frames": [[576, 672], [672, 672], [768, 672], [864, 672]], "switchOut": { "msPerFrame": 100, "frames": [[96, 864], [192, 864], [288, 864], [384, 864], [480, 864], [576, 864], [672, 864], [768, 864]] @@ -213,11 +241,15 @@ "switchIn": { "msPerFrame": 100, "frames": [[768, 864], [672, 864], [576, 864], [480, 864], [384, 864], [288, 864], [192, 864], [96, 864]] + }, + "damageFront": { + "msPerFrame": 100, + "frames": [[960, 672], [1056, 672], [0, 768], [96, 768]] } }, - "malalien_back.gif": { + "pengym_back.gif": { "msPerFrame": 200, - "frames": [[576, 576], [672, 576]], + "frames": [[192, 768], [288, 768]], "switchOut": { "msPerFrame": 100, "frames": [[864, 864], [960, 864], [1056, 864], [1152, 864], [1248, 864], [1344, 864], [0, 960], [96, 960]] @@ -227,9 +259,9 @@ "frames": [[96, 960], [0, 960], [1344, 864], [1248, 864], [1152, 864], [1056, 864], [960, 864], [864, 864]] } }, - "malalien_front.gif": { + "pengym_front.gif": { "msPerFrame": 100, - "frames": [[768, 576], [864, 576], [960, 576], [0, 672]], + "frames": [[384, 768], [480, 768], [576, 768], [672, 768]], "switchOut": { "msPerFrame": 100, "frames": [[192, 960], [288, 960], [384, 960], [480, 960], [576, 960], [672, 960], [768, 960], [864, 960]] @@ -237,11 +269,15 @@ "switchIn": { "msPerFrame": 100, "frames": [[864, 960], [768, 960], [672, 960], [576, 960], [480, 960], [384, 960], [288, 960], [192, 960]] + }, + "damageFront": { + "msPerFrame": 100, + "frames": [[768, 768], [864, 768], [960, 768], [1056, 768]] } }, - "pengym_back.gif": { + "sofabbi_back.gif": { "msPerFrame": 200, - "frames": [[96, 672], [192, 672]], + "frames": [[0, 864], [96, 864]], "switchOut": { "msPerFrame": 100, "frames": [[960, 960], [1056, 960], [1152, 960], [1248, 960], [1344, 960], [0, 1056], [96, 1056], [192, 1056]] @@ -251,9 +287,9 @@ "frames": [[192, 1056], [96, 1056], [0, 1056], [1344, 960], [1248, 960], [1152, 960], [1056, 960], [960, 960]] } }, - "pengym_front.gif": { - "msPerFrame": 100, - "frames": [[288, 672], [384, 672], [480, 672], [576, 672]], + "sofabbi_front.gif": { + "msPerFrame": 200, + "frames": [[192, 864], [288, 864], [384, 864], [480, 864]], "switchOut": { "msPerFrame": 100, "frames": [[288, 1056], [384, 1056], [480, 1056], [576, 1056], [672, 1056], [768, 1056], [864, 1056], [960, 1056]] @@ -261,11 +297,15 @@ "switchIn": { "msPerFrame": 100, "frames": [[960, 1056], [864, 1056], [768, 1056], [672, 1056], [576, 1056], [480, 1056], [384, 1056], [288, 1056]] + }, + "damageFront": { + "msPerFrame": 100, + "frames": [[576, 864], [672, 864], [768, 864], [864, 864]] } }, - "sofabbi_back.gif": { + "volthare_back.gif": { "msPerFrame": 200, - "frames": [[672, 672], [768, 672]], + "frames": [[960, 864], [1056, 864]], "switchOut": { "msPerFrame": 100, "frames": [[1056, 1056], [1152, 1056], [1248, 1056], [1344, 1056], [0, 1152], [96, 1152], [192, 1152], [288, 1152]] @@ -275,9 +315,9 @@ "frames": [[288, 1152], [192, 1152], [96, 1152], [0, 1152], [1344, 1056], [1248, 1056], [1152, 1056], [1056, 1056]] } }, - "sofabbi_front.gif": { - "msPerFrame": 200, - "frames": [[864, 672], [960, 672], [0, 768], [96, 768]], + "volthare_front.gif": { + "msPerFrame": 100, + "frames": [[0, 960], [96, 960], [192, 960], [288, 960]], "switchOut": { "msPerFrame": 100, "frames": [[384, 1152], [480, 1152], [576, 1152], [672, 1152], [768, 1152], [864, 1152], [960, 1152], [1056, 1152]] @@ -285,11 +325,15 @@ "switchIn": { "msPerFrame": 100, "frames": [[1056, 1152], [960, 1152], [864, 1152], [768, 1152], [672, 1152], [576, 1152], [480, 1152], [384, 1152]] + }, + "damageFront": { + "msPerFrame": 100, + "frames": [[384, 960], [480, 960], [576, 960], [672, 960]] } }, - "volthare_back.gif": { + "xmon_back.gif": { "msPerFrame": 200, - "frames": [[192, 768], [288, 768]], + "frames": [[768, 960], [864, 960]], "switchOut": { "msPerFrame": 100, "frames": [[1152, 1152], [1248, 1152], [1344, 1152], [0, 1248], [96, 1248], [192, 1248], [288, 1248], [384, 1248]] @@ -299,9 +343,9 @@ "frames": [[384, 1248], [288, 1248], [192, 1248], [96, 1248], [0, 1248], [1344, 1152], [1248, 1152], [1152, 1152]] } }, - "volthare_front.gif": { + "xmon_front.gif": { "msPerFrame": 100, - "frames": [[384, 768], [480, 768], [576, 768], [672, 768]], + "frames": [[960, 960], [1056, 960], [0, 1056], [96, 1056]], "switchOut": { "msPerFrame": 100, "frames": [[480, 1248], [576, 1248], [672, 1248], [768, 1248], [864, 1248], [960, 1248], [1056, 1248], [1152, 1248]] @@ -309,30 +353,10 @@ "switchIn": { "msPerFrame": 100, "frames": [[1152, 1248], [1056, 1248], [960, 1248], [864, 1248], [768, 1248], [672, 1248], [576, 1248], [480, 1248]] - } - }, - "xmon_back.gif": { - "msPerFrame": 200, - "frames": [[768, 768], [864, 768]], - "switchOut": { - "msPerFrame": 100, - "frames": [[1248, 1248], [1344, 1248], [0, 1344], [96, 1344], [192, 1344], [288, 1344], [384, 1344], [480, 1344]] }, - "switchIn": { - "msPerFrame": 100, - "frames": [[480, 1344], [384, 1344], [288, 1344], [192, 1344], [96, 1344], [0, 1344], [1344, 1248], [1248, 1248]] - } - }, - "xmon_front.gif": { - "msPerFrame": 100, - "frames": [[960, 768], [0, 864], [96, 864], [192, 864]], - "switchOut": { - "msPerFrame": 100, - "frames": [[576, 1344], [672, 1344], [768, 1344], [864, 1344], [960, 1344], [1056, 1344], [1152, 1344], [1248, 1344]] - }, - "switchIn": { + "damageFront": { "msPerFrame": 100, - "frames": [[1248, 1344], [1152, 1344], [1056, 1344], [960, 1344], [864, 1344], [768, 1344], [672, 1344], [576, 1344]] + "frames": [[192, 1056], [288, 1056], [384, 1056], [480, 1056]] } } } \ No newline at end of file diff --git a/drool/imgs/volthare_front_damage.gif b/drool/imgs/volthare_front_damage.gif new file mode 100644 index 0000000000000000000000000000000000000000..1c4c98598c2d59be8b0ebab8a50c26074d9cbf04 GIT binary patch literal 6676 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 list[Path]: + """Rename *_damage_front.png -> *_front_damage.png in place. Returns the + full set of *_front_damage.png paths (including any that were already + correctly named).""" + for old in sorted(source_dir.glob("*_damage_front.png")): + new = old.with_name(old.name.replace("_damage_front.png", "_front_damage.png")) + if new.exists() and new != old: + print(f" ⚠ Skipping rename, target exists: {new.name}") + continue + old.rename(new) + print(f" ✓ Renamed {old.name} -> {new.name}") + return sorted(source_dir.glob("*_front_damage.png")) + + +def png_sheet_to_gif(png_path: Path, gif_path: Path) -> None: + """Slice a horizontal NUM_FRAMES x 1 sprite sheet into individual frames and + save as an animated GIF.""" + sheet = Image.open(png_path).convert("RGBA") + expected = (FRAME_SIZE * NUM_FRAMES, FRAME_SIZE) + if sheet.size != expected: + raise ValueError(f"{png_path.name}: expected {expected[0]}x{expected[1]}, got {sheet.size[0]}x{sheet.size[1]}") + + frames = [ + sheet.crop((i * FRAME_SIZE, 0, (i + 1) * FRAME_SIZE, FRAME_SIZE)) + for i in range(NUM_FRAMES) + ] + frames[0].save( + gif_path, + save_all=True, + append_images=frames[1:], + duration=FRAME_DURATION_MS, + loop=0, + disposal=2, + transparency=0, + optimize=False, + ) + + +def run(source_dir: str = None, output_dir: str = None) -> bool: + base = Path(__file__).parent.parent + src = Path(source_dir) if source_dir else base / "drool" / "imgs" + out = Path(output_dir) if output_dir else base / "drool" / "imgs" + + if not src.is_dir(): + print(f"Error: source dir '{src}' does not exist") + return False + out.mkdir(parents=True, exist_ok=True) + + print(f"Renaming damage sheets in: {src}") + pngs = rename_damage_pngs(src) + if not pngs: + print("No *_damage_front.png or *_front_damage.png files found") + return False + + print(f"\nConverting {len(pngs)} sheets to GIFs in: {out}") + for png in pngs: + gif_path = out / (png.stem + ".gif") + png_sheet_to_gif(png, gif_path) + print(f" ✓ {png.name} -> {gif_path.name}") + + print("\n✅ Done!") + return True + + +def main() -> None: + src = sys.argv[1] if len(sys.argv) >= 2 else None + out = sys.argv[2] if len(sys.argv) >= 3 else None + if not run(src, out): + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/processing/createMonSpritesheets.py b/processing/createMonSpritesheets.py index da442048..fb925129 100644 --- a/processing/createMonSpritesheets.py +++ b/processing/createMonSpritesheets.py @@ -132,7 +132,17 @@ def create_spritesheets(gif_files: list[str], output_dir: str): metadata = {} all_frames, all_switch_frames = [], [] - for gif_path in gif_files: + # Damage gifs (e.g. aurox_front_damage.gif) are not standalone entries — + # their frames get attached to the matching {mon}_front.gif entry as a + # `damageFront` sub-key, and are packed into the spritesheet immediately + # after the parent's idle frames so related frames stay contiguous. + regular_gifs = [g for g in gif_files if "_front_damage" not in Path(g).name.lower()] + damage_gifs = [g for g in gif_files if "_front_damage" in Path(g).name.lower()] + damage_by_parent = { + Path(g).name.replace("_front_damage.gif", "_front.gif"): g for g in damage_gifs + } + + for gif_path in regular_gifs: name = Path(gif_path).name frames, frame_rate = extract_frames(gif_path) print(f"Extracted {len(frames)} frames from {name} (frame rate: {frame_rate}ms)") @@ -142,6 +152,18 @@ def create_spritesheets(gif_files: list[str], output_dir: str): all_frames.extend(frames) metadata[name] = {"msPerFrame": frame_rate, "_main_start": frame_start, "_main_count": len(frames)} + # Damage frames immediately follow the idle frames for this mon + if name in damage_by_parent: + damage_path = damage_by_parent.pop(name) + damage_name = Path(damage_path).name + d_frames, d_rate = extract_frames(damage_path) + damage_start = len(all_frames) + all_frames.extend(d_frames) + metadata[name]["_damage_start"] = damage_start + metadata[name]["_damage_count"] = len(d_frames) + metadata[name]["_damage_rate"] = d_rate + print(f"Extracted {len(d_frames)} damage frames from {damage_name} -> {name} (frame rate: {d_rate}ms)") + # Switch animation frames white = to_white_silhouette(frames[0]) morph = create_morph_animation(white, is_back="back" in name.lower()) @@ -151,6 +173,9 @@ def create_spritesheets(gif_files: list[str], output_dir: str): metadata[name]["_switch_count"] = len(morph) print(f"Generated {len(morph)} switch frames for {name}") + for parent in damage_by_parent: + print(f"⚠ No matching {parent} entry for {parent.replace('_front.gif', '_front_damage.gif')}, skipping") + # Build main spritesheet sheet, positions = build_spritesheet(all_frames) output_path = Path(output_dir) @@ -189,6 +214,14 @@ def create_spritesheets(gif_files: list[str], output_dir: str): switch_frames = [list(switch_positions[switch_start + i]) for i in range(switch_count)] data["switchOut"] = {"msPerFrame": 100, "frames": switch_frames} data["switchIn"] = {"msPerFrame": 100, "frames": switch_frames[::-1]} + if "_damage_start" in data: + d_start = data.pop("_damage_start") + d_count = data.pop("_damage_count") + d_rate = data.pop("_damage_rate") + data["damageFront"] = { + "msPerFrame": d_rate, + "frames": [list(positions[d_start + i]) for i in range(d_count)], + } # Save JSON with compact coordinate arrays json_path = Path(output_dir) / "spritesheet.json" diff --git a/processing/generateMonsTypeScript.py b/processing/generateMonsTypeScript.py index bde93295..9194d37e 100644 --- a/processing/generateMonsTypeScript.py +++ b/processing/generateMonsTypeScript.py @@ -95,6 +95,7 @@ def build_sprites( ("front", "frontIdle", None, "mon_spritesheet.png", True), ("front", "frontSwitchIn", "switchIn", "mon_switch.png", False), ("front", "frontSwitchOut", "switchOut", "mon_switch.png", False), + ("front", "frontHurt", "damageFront", "mon_spritesheet.png", False), ("back", "backIdle", None, "mon_spritesheet.png", True), ("back", "backSwitchIn", "switchIn", "mon_switch.png", False), ("back", "backSwitchOut", "switchOut", "mon_switch.png", False), @@ -353,9 +354,11 @@ def generate_typescript_const(data: Dict[int, Dict[str, Any]], output_file: str) readonly frontIdle: SpriteAnimationConfig; readonly frontSwitchIn: SpriteAnimationConfig; readonly frontSwitchOut: SpriteAnimationConfig; + readonly frontHurt?: SpriteAnimationConfig; readonly backIdle: SpriteAnimationConfig; readonly backSwitchIn: SpriteAnimationConfig; readonly backSwitchOut: SpriteAnimationConfig; + readonly backHurt?: SpriteAnimationConfig; }}; readonly stats: {{ readonly hp: number; diff --git a/transpiler/MIGRATION.md b/transpiler/MIGRATION.md deleted file mode 100644 index ee4cf6aa..00000000 --- a/transpiler/MIGRATION.md +++ /dev/null @@ -1,112 +0,0 @@ -# Migration Plan — Extracting `extruder` as a Standalone Library - -## 7. Milestones - -Proposed ordering. Each is a PR-sized chunk. - -### M0 — Repo split (physical) -- [ ] Create `extruder` repo with a copy of `transpiler/`. -- [ ] Strip Chomp-specific `skipFiles`, `skipDirs`, `dependencyOverrides` from the - library's `transpiler-config.json`. Chomp keeps its own config. -- [x] ~~Delete `runtime/battle-harness.ts` from the library. (Move to Chomp.)~~ - Lives at `munch/src/app/ts-output/runtime/battle-harness.ts`. Munch - owns its own `runtime/`; chomp's deploy rsync excludes `runtime/` so - munch-only files aren't clobbered. Chomp's `transpiler/runtime/` no - longer re-exports `BattleHarness`. -- [x] ~~Move Chomp-specific vitest tests out of the transpiler tree.~~ All 30 - Chomp-specific vitest tests + `fixtures/mocks.ts` removed. Extruder - doesn't ship a JS test suite: `package.json`, `package-lock.json`, - `bun.lock`, `vitest.config.ts`, and `node_modules/` are all deleted. - Extruder is a Python tool; regression coverage lives in - `test_transpiler.py`. Consumer-side runtime behavior verification is - the consumer's job, run against their own `ts-output/` with whatever - JS test runner they prefer. -- [ ] Move `runtime/Ownable.ts`, `runtime/EIP712.ts`, - `runtime/EnumerableSetLib.ts` into `examples/runtime-replacements/`. - Also move `runtime/ECDSA.ts` into examples. - Drop their `runtimeReplacements` entries from the library's shipped config. - Chomp copies them into its own repo and re-adds its config entries. -- [ ] Chomp continues to use its vendored `transpiler/` directory — no consumer - switchover yet. - -### M1 — Core decontamination -- [x] ~~Remove `_DEFAULT_INTERFACE_ALIASES` hardcoded map.~~ **Done.** - `DependencyResolver` now reads `interfaceAliases` from config. Chomp's - `transpiler-config.json` re-adds the aliases Chomp needs (IEngine → - Engine, etc.). Transpile output byte-identical. -- [x] ~~Remove `hashBattle`, `hashBattleOffer` from `codegen/expression.py:889`.~~ - Done via `TypeRegistry` return-type lookup instead of a config field — see §2b. -- [x] ~~Rewrite `sol2ts.py` docstring, `package.json` description.~~ **Done.** - Docstring now leads with the extruder framing; `package.json` renamed to - `extruder` at `0.1.0-alpha`. -- [x] ~~Chomp's `transpiler-config.json` adds its interface aliases back in.~~ - **Done.** Included above in the alias removal task. - -### M2 — Clone-and-run hygiene -- [ ] Write `requirements.txt` pinning Python deps (currently managed via `uv`). -- [ ] Add a one-line install check in `extruder.py` that errors helpfully if deps are - missing (e.g. pexpect, pillow, numpy). -- [ ] Confirm `python3 extruder.py --help` works from a fresh clone. - -### M2.5 — Runtime-replacement stub flow (§2f) -- [x] ~~Remove or repurpose the dead `--stub ` flag.~~ **Done.** Flag - and `stubbed_contracts` plumbing removed from `sol2ts.py` (also removed from - `SolidityToTypeScriptTranspiler.__init__`). -- [x] ~~Add `--emit-replacement-stub `.~~ **Done.** - Implemented in `transpiler/codegen/replacement_stub.py` and wired through - `sol2ts.py`. Emits a TypeScript class with mapped method signatures, - `throw new Error('Not implemented')` bodies, default state-variable values, - constants rendered as `static readonly` with literal values when the parser - can extract them, local struct fields rendered as sibling TS interfaces, and - Solidity overloads disambiguated with `_overloadN` suffixes + a TODO comment. - Prints the ready-to-paste `runtimeReplacements` entry (including a - registry-derived `interface` block) to stdout. -- [x] ~~Validate end-to-end~~ **Done.** Regenerated `Ownable.ts` / `ECDSA.ts` / - `EIP712.ts` / `EnumerableSetLib.ts` from `src/lib/*.sol`; each stub - type-checks cleanly against `runtime/base.ts` under `tsc --strict`. Chomp's - normal transpile is byte-identical; full Python test suite stays green. -- [ ] Document the flow in `docs/runtime-replacements.md` with ECDSA as the worked - example. (Follow-up in M3.) - -### M2.75 — `extruder init` bootstrap command (§2g) -- [x] Scan phase (`transpiler/init.py::scan()`) — pure, returns `InitReport`. -- [x] Unit tests covering every verdict path, including false-positive guards - (magic-number `sstore` → OK, not REPLACE; `new bytes(n)` → OK). -- [x] `build_plan` + `apply` split, with `Prompter` behind a mockable seam. -- [x] CLI wiring: `extruder init [--yes] [--stub-output-dir] [--config-path]`. -- [x] Integrated with `--emit-replacement-stub` via an injected `stub_emitter` - callable (keeps the module acyclic). -- [x] Dependency-resolver dry-run phase — `MetadataExtractor` + `DependencyResolver` - walk every constructor; unresolved entries become prompts (or auto-mapped - when a single implementer exists, or punted in `--yes` mode). -- [x] Dog-fooded against Chomp: 112 OK, 8 REPLACE (all legitimate), 0 false - positives. Auto-maps 12 single-impl interfaces, flags `IAbility` + - `IMoveSet` as tag interfaces, resolves 8 of 9 dep-dry-run entries via - aliases, punts `GachaRegistry._MON_REGISTRY` (multi-impl). -- [x] Config merge-conflict prompts — plan consults the existing config and - prompts on conflicting alias/override entries; under `--yes` the - existing value wins silently and the conflict is logged. -- [x] AST-level MAYBE detection — files with modifiers (W001) or - `receive`/`fallback` (W003) surface as MAYBE with specific reasons. - Receive/fallback is detected via a source-text regex because the - parser drops those tokens before the AST. -- [ ] Follow-up: trial-transpile-based MAYBE detection for W002 (try/catch) - and W004 (function pointers). Would require running the generator; - deferred until we see real user demand. - -### M3 — Docs (see §6) -- [x] `README.md` rewritten (~160 lines, down from 370). Leads with the - extruder framing and `extruder init`; full TOC links into `docs/`. -- [x] `docs/quickstart.md`, `docs/init.md`, `docs/configuration.md`, - `docs/runtime-replacements.md`, `docs/runtime.md`, - `docs/semantics.md`, `docs/extending.md` — all written. -- [x] `docs/faq.md` seeded with the common starter-question pointers; - kept deliberately empty until real user questions arrive. - -### M4 — Examples + tests -- [ ] `examples/erc20/` runnable example. -- [ ] Transpiler integration tests against the fixtures (`minimal`, `inheritance`, - `yul`, `mappings`). - ---- - From 61b00051a4a300de3018088a1b6d87f4f717de3c Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Mon, 4 May 2026 16:08:57 -0700 Subject: [PATCH 09/10] Narrow extraData from uint240 to uint16 and update salt types (#66) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Phase 0: dual-sig security fix + extraData/salt width narrowing Per OPT_PLAN.md §9 Phase 0, in preparation for the batched-execute work: 1. Security fix to `SignedCommitManager.executeWithDualSignedMoves`: - Add `bytes calldata committerSignature` parameter before `revealerSignature`. - Recover committer signature against `SignedCommit{committerMoveHash, battleKey, turnId}`; require equality with the committer. - Drop the fragile `msg.sender == committer` check (and `error CallerNotCommitter`). The function is now relayer-friendly: anyone with both valid signatures + the committer preimage can submit. - Closes the "unilateral revealer" attack: previously a malicious revealer could sign `DualSignedReveal{committerMoveHash: keccak(P*), …}` for any preimage `P*` and submit (when the msg.sender check evolved away, batching, etc.). Now the explicit committer signature binds the committer cryptographically. 2. Width narrowing (clean break, no shims) — touched together because `DualSignedReveal`'s EIP-712 typehash includes both fields: - `extraData`: 240 → 16 bits across `IMoveSet`, all 50+ mon moves/abilities, `Engine`/`IEngine` (`setMove`, `executeWithMoves`, `executeWithSingleMove`, `validatePlayerMoveForBattle`, `MoveDecision.extraData`, `RevealedMove.extraData`), `IValidator`/`DefaultValidator`, `ValidatorLogic`, the commit managers, the CPU stack, and `SleepStatus`. - `salt`: 256 → 104 bits across the same surface, plus the engine's `_turnP{0,1}Salt` transients, `BattleConfig{,View}.{p0,p1}Salt`, `RevealedMove.salt`, and the `MonMove` event signature. - `SignedCommitLib.DualSignedReveal.revealerSalt` and the matching EIP-712 typehash updated to `uint104` / `uint16`. - Random-oracle interface kept as `bytes32` (it's an arbitrary-input hash surface, not a security boundary); `Engine` casts at the call site. - Test mocks repacked into 16 bits with explicit layouts: - `_packStatBoost` / `StatBoostsMove`: `[boost:8 | stat:4 | mon:3 | player:1]` - `_packForceSwitch` / `ForceSwitchMove`: `[mon:15 | player:1]` - `EditEffectAttack`: `[effectIdx:10 | monIdx:4 | targetIdx:2]` - `MockKVWriterMove`: `[value:6 | key:10]` - `MockEffectRemover`: switched from passing the effect address (no longer fits) to passing the slot index from `getEffects`. 3. New TDD regression tests in `SignedCommitManager.t.sol`: - `test_executeWithDualSigned_thirdPartyRelay_succeeds` — drops msg.sender check; arbitrary relayer with both valid sigs can submit. - `test_revert_executeWithDualSigned_unilateralRevealerAttack` — revealer alone (forging committer sig with their own key) reverts on the committer-sig recovery. - `test_revert_executeWithDualSigned_wrongCommitterSigner` — committer sig recovers to a non-committer address. - `test_revert_executeWithDualSigned_committerSigForWrongHash` — committer signs a different `moveHash` than the submitted preimage → engine recomputes from preimage, mismatch reverts. 4. Imported the OPT_PLAN.md and BatchInstrumentationTest.sol scaffolding from `feat/next` so subsequent phases land on the same plan. Two `BetterCPU` strategy tests (`test_betterCPUSelectsHighestDamageMove`, `test_defensiveSwitch_materialityNotMet`) needed a CPU-mon speed bump to deterministically break the speed tie: with old 256-bit salts the salt-derived RNG happened to give CPU priority, with 104-bit salts it doesn't, so the original tests' assumption was implicit. The fix is to make the priority deterministic instead of leaning on RNG. Gas snapshots refreshed across the affected suites — dual-signed flow gains the second `ecrecover`, partly offset by narrower calldata. `forge build` clean. `forge test`: 352/352 passing. https://claude.ai/code/session_01Lc98i85bMi3SWTBnaNiDuZ * Phase 0 cleanup: dedupe test helpers, trim narrative comments Per /simplify review of Phase 0: 1. **Consolidate EIP-712 signing helpers.** Phase 0 introduced four byte-identical copies of `_signCommit` and grew `_signDualReveal` to four copies (it had two pre-Phase 0). Added `test/abstract/SignedCommitHelper.sol` — a standalone abstract that takes the verifying-contract address as a parameter (no `EIP712` inheritance needed; `_DOMAIN_TYPEHASH` is replicated as a constant) and is inherited by `SignedCommitManager.t.sol`, `BatchInstrumentationTest`, `InlineEngineGasTest::FullyOptimizedInlineGasTest`, and `StandardAttackPvPGasTest`. Drops ~280 lines and the `_domainNameAndVersion` override boilerplate from each. 2. **Move `_packStatBoost` to `BattleHelper`.** Three byte-identical copies (load-bearing — the bit layout must stay in lockstep with `StatBoostsMove`'s decoder) collapsed into a single home. 3. **Use existing `_createMonWithSpeed` helper in `BetterCPUTest.sol`.** The two Phase 0 edits open-coded `_createMon(...); mon.stats.speed = 100;` — replaced with the helper that already exists at line 77. 4. **Trim narrative `// what` comments from `SignedCommitManager.sol` per CLAUDE.md guidance** (default to no comments; only WHY). Kept the stack-pressure rationale on the scoped recovery blocks and the natdoc on the function itself (which carries the real WHY about the unilateral-revealer attack). 5. **Add a one-line WHY for the salt truncation in `CPUMoveManager.sol`** so the deliberate 256→104-bit narrowing isn't cargo-culted as accidental. Build clean. `forge test`: 352/352 passing. Gas snapshots refreshed where the helper-extraction path shifted dispatch overhead. Skipped findings (deliberate): - Hoisting `_getCurrentTurnSalt` calls in `Engine.sol` to save one or two transient loads per turn — pre-existing inefficiency, scope of the later helper-extraction phase (§9 Phase 0.5). - Tightening `MockKVWriterMove`'s misleading `uint192 value` to a true 6-bit type with bound checks — works for current tests, value-range guard would be useful but the mock is test-only. https://claude.ai/code/session_01Lc98i85bMi3SWTBnaNiDuZ --------- Co-authored-by: Claude --- snapshots/BetterCPUInlineGasTest.json | 12 +- snapshots/EngineGasTest.json | 36 +- snapshots/EngineOptimizationTest.json | 4 +- snapshots/FullyOptimizedInlineGasTest.json | 12 +- snapshots/InlineEngineGasTest.json | 28 +- snapshots/MatchmakerTest.json | 6 +- src/DefaultValidator.sol | 6 +- src/Engine.sol | 42 +- src/IEngine.sol | 14 +- src/IValidator.sol | 4 +- src/Structs.sol | 16 +- src/commit-manager/DefaultCommitManager.sol | 2 +- src/commit-manager/ICommitManager.sol | 2 +- src/commit-manager/SignedCommitLib.sol | 8 +- src/commit-manager/SignedCommitManager.sol | 91 ++-- src/cpu/BetterCPU.sol | 10 +- src/cpu/CPU.sol | 30 +- src/cpu/CPUMoveManager.sol | 8 +- src/cpu/ICPU.sol | 4 +- src/cpu/OkayCPU.sol | 6 +- src/cpu/PlayerCPU.sol | 8 +- src/cpu/RandomCPU.sol | 4 +- src/effects/status/SleepStatus.sol | 2 +- src/lib/ValidatorLogic.sol | 2 +- src/mons/aurox/BullRush.sol | 2 +- src/mons/aurox/GildedRecovery.sol | 6 +- src/mons/aurox/IronWall.sol | 4 +- src/mons/aurox/VolatilePunch.sol | 2 +- src/mons/ekineki/BubbleBop.sol | 2 +- src/mons/ekineki/NineNineNine.sol | 4 +- src/mons/ekineki/Overflow.sol | 2 +- src/mons/ekineki/SneakAttack.sol | 4 +- src/mons/embursa/HeatBeacon.sol | 4 +- src/mons/embursa/HoneyBribe.sol | 4 +- src/mons/embursa/Q5.sol | 4 +- src/mons/embursa/SetAblaze.sol | 2 +- src/mons/ghouliath/EternalGrudge.sol | 4 +- src/mons/ghouliath/WitherAway.sol | 2 +- src/mons/gorillax/RockPull.sol | 4 +- src/mons/iblivion/Brightback.sol | 4 +- src/mons/iblivion/Loop.sol | 4 +- src/mons/iblivion/Renormalize.sol | 4 +- src/mons/iblivion/UnboundedStrike.sol | 4 +- src/mons/inutia/ChainExpansion.sol | 4 +- src/mons/inutia/HitAndDip.sol | 4 +- src/mons/inutia/Initialize.sol | 4 +- src/mons/malalien/TripleThink.sol | 4 +- src/mons/pengym/Deadlift.sol | 4 +- src/mons/pengym/DeepFreeze.sol | 4 +- src/mons/pengym/PistolSquat.sol | 2 +- src/mons/sofabbi/Gachachacha.sol | 4 +- src/mons/sofabbi/GuestFeature.sol | 4 +- src/mons/sofabbi/SnackBreak.sol | 4 +- src/mons/volthare/DualShock.sol | 2 +- src/mons/volthare/MegaStarBlast.sol | 4 +- src/mons/volthare/RoundTrip.sol | 4 +- src/mons/xmon/ContagiousSlumber.sol | 4 +- src/mons/xmon/NightTerrors.sol | 4 +- src/mons/xmon/Somniphobia.sol | 4 +- src/mons/xmon/VitalSiphon.sol | 2 +- src/moves/IMoveSet.sol | 4 +- src/moves/StandardAttack.sol | 4 +- test/BattleHistoryTest.sol | 10 +- test/BetterCPUInlineGasTest.sol | 16 +- test/BetterCPUTest.sol | 171 +++---- test/CPUTest.sol | 46 +- test/DefaultCommitManagerTest.sol | 14 +- test/EngineGasTest.sol | 63 ++- test/EngineGlobalKVTest.sol | 25 +- test/EngineOptimizationTest.sol | 44 +- test/EngineTest.sol | 180 +++---- test/GachaTest.sol | 8 +- test/InlineAbilityParityTest.sol | 18 +- test/InlineEngineGasTest.sol | 180 +++---- test/InlineMoveParityTest.sol | 10 +- test/InlineValidationTest.sol | 40 +- test/MatchmakerTest.sol | 2 +- test/SignedCommitManager.t.sol | 510 +++++++++++--------- test/SignedCommitManagerGasBenchmark.t.sol | 66 +-- test/StandardAttackPvPGasTest.sol | 63 +-- test/abstract/BattleHelper.sol | 25 +- test/abstract/SignedCommitHelper.sol | 72 +++ test/effects/EffectTest.sol | 28 +- test/effects/StatBoosts.t.sol | 19 +- test/mocks/CustomAttack.sol | 4 +- test/mocks/EditEffectAttack.sol | 13 +- test/mocks/EffectAttack.sol | 4 +- test/mocks/ForceSwitchMove.sol | 10 +- test/mocks/GlobalEffectAttack.sol | 4 +- test/mocks/InvalidMove.sol | 4 +- test/mocks/MockEffectRemover.sol | 24 +- test/mocks/MockKVWriterMove.sol | 15 +- test/mocks/ReduceSpAtkMove.sol | 4 +- test/mocks/SelfSwitchAndDamageMove.sol | 4 +- test/mocks/SkipTurnMove.sol | 4 +- test/mocks/StatBoostsMove.sol | 14 +- test/mocks/TestMoveFactory.sol | 4 +- test/mons/AuroxTest.sol | 28 +- test/mons/EkinekiTest.sol | 42 +- test/mons/EmbursaTest.sol | 24 +- test/mons/GhouliathTest.sol | 22 +- test/mons/GorillaxTest.sol | 8 +- test/mons/IblivionTest.sol | 44 +- test/mons/InutiaTest.sol | 24 +- test/mons/MalalienTest.sol | 10 +- test/mons/PengymTest.sol | 48 +- test/mons/SofabbiTest.sol | 20 +- test/mons/VolthareTest.sol | 16 +- test/mons/XmonTest.sol | 20 +- test/moves/StandardAttackRngTest.sol | 2 +- 110 files changed, 1308 insertions(+), 1211 deletions(-) create mode 100644 test/abstract/SignedCommitHelper.sol diff --git a/snapshots/BetterCPUInlineGasTest.json b/snapshots/BetterCPUInlineGasTest.json index ae3a2b0a..b2978ec6 100644 --- a/snapshots/BetterCPUInlineGasTest.json +++ b/snapshots/BetterCPUInlineGasTest.json @@ -1,8 +1,8 @@ { - "Flag0_P0ForcedSwitch": "22701", - "Turn0_Lead": "104410", - "Turn1_BothAttack": "262381", - "Turn2_BothAttack": "236457", - "Turn3_BothAttack": "232481", - "Turn4_BothAttack": "232485" + "Flag0_P0ForcedSwitch": "22763", + "Turn0_Lead": "104184", + "Turn1_BothAttack": "264088", + "Turn2_BothAttack": "238164", + "Turn3_BothAttack": "234188", + "Turn4_BothAttack": "234192" } \ No newline at end of file diff --git a/snapshots/EngineGasTest.json b/snapshots/EngineGasTest.json index 844a9603..8d0b40ce 100644 --- a/snapshots/EngineGasTest.json +++ b/snapshots/EngineGasTest.json @@ -1,21 +1,21 @@ { - "B1_Execute": "889458", - "B1_Setup": "816408", - "B2_Execute": "645719", - "B2_Setup": "282218", - "Battle1_Execute": "449877", - "Battle1_Setup": "791633", - "Battle2_Execute": "367369", - "Battle2_Setup": "233097", - "External_Execute": "456527", - "External_Setup": "782348", - "FirstBattle": "2906000", - "Inline_Execute": "318805", - "Inline_Setup": "225023", + "B1_Execute": "889886", + "B1_Setup": "816459", + "B2_Execute": "648291", + "B2_Setup": "282290", + "Battle1_Execute": "449213", + "Battle1_Setup": "791685", + "Battle2_Execute": "368905", + "Battle2_Setup": "233149", + "External_Execute": "456719", + "External_Setup": "782400", + "FirstBattle": "2911711", + "Inline_Execute": "320015", + "Inline_Setup": "225083", "Intermediary stuff": "44458", - "SecondBattle": "2927592", - "Setup 1": "1670484", - "Setup 2": "292493", - "Setup 3": "332770", - "ThirdBattle": "2274723" + "SecondBattle": "2936650", + "Setup 1": "1670530", + "Setup 2": "292539", + "Setup 3": "332805", + "ThirdBattle": "2282494" } \ No newline at end of file diff --git a/snapshots/EngineOptimizationTest.json b/snapshots/EngineOptimizationTest.json index 883b347d..09b37c45 100644 --- a/snapshots/EngineOptimizationTest.json +++ b/snapshots/EngineOptimizationTest.json @@ -1,4 +1,4 @@ { - "ExternalStaminaRegen": "390772", - "InlineStaminaRegen": "999942" + "ExternalStaminaRegen": "392424", + "InlineStaminaRegen": "999051" } \ No newline at end of file diff --git a/snapshots/FullyOptimizedInlineGasTest.json b/snapshots/FullyOptimizedInlineGasTest.json index 3edf6547..66ef6ab8 100644 --- a/snapshots/FullyOptimizedInlineGasTest.json +++ b/snapshots/FullyOptimizedInlineGasTest.json @@ -1,8 +1,8 @@ { - "Fast_Battle1": "1802444", - "Fast_Battle2": "1704099", - "Fast_Battle3": "1221705", - "Fast_Setup_1": "1321867", - "Fast_Setup_2": "216314", - "Fast_Setup_3": "212698" + "Fast_Battle1": "1867564", + "Fast_Battle2": "1777864", + "Fast_Battle3": "1288656", + "Fast_Setup_1": "1322088", + "Fast_Setup_2": "216706", + "Fast_Setup_3": "212909" } \ No newline at end of file diff --git a/snapshots/InlineEngineGasTest.json b/snapshots/InlineEngineGasTest.json index ffc94073..d4833e98 100644 --- a/snapshots/InlineEngineGasTest.json +++ b/snapshots/InlineEngineGasTest.json @@ -1,16 +1,16 @@ { - "B1_Execute": "870357", - "B1_Setup": "758497", - "B2_Execute": "604941", - "B2_Setup": "271670", - "Battle1_Execute": "402355", - "Battle1_Setup": "733715", - "Battle2_Execute": "318757", - "Battle2_Setup": "224451", - "FirstBattle": "2595237", - "SecondBattle": "2580140", - "Setup 1": "1612152", - "Setup 2": "318169", - "Setup 3": "314901", - "ThirdBattle": "1964987" + "B1_Execute": "870101", + "B1_Setup": "758557", + "B2_Execute": "606857", + "B2_Setup": "271758", + "Battle1_Execute": "401230", + "Battle1_Setup": "733775", + "Battle2_Execute": "319832", + "Battle2_Setup": "224511", + "FirstBattle": "2595770", + "SecondBattle": "2583418", + "Setup 1": "1612191", + "Setup 2": "318239", + "Setup 3": "314704", + "ThirdBattle": "1967495" } \ No newline at end of file diff --git a/snapshots/MatchmakerTest.json b/snapshots/MatchmakerTest.json index 9b3b0b0c..344f7a99 100644 --- a/snapshots/MatchmakerTest.json +++ b/snapshots/MatchmakerTest.json @@ -1,5 +1,5 @@ { - "Accept1": "307765", - "Accept2": "34130", - "Propose1": "197286" + "Accept1": "307809", + "Accept2": "34162", + "Propose1": "197318" } \ No newline at end of file diff --git a/src/DefaultValidator.sol b/src/DefaultValidator.sol index d2659ed1..e9f35c71 100644 --- a/src/DefaultValidator.sol +++ b/src/DefaultValidator.sol @@ -103,7 +103,7 @@ contract DefaultValidator is IValidator { bytes32 battleKey, uint256 moveIndex, uint256 playerIndex, - uint240 extraData + uint16 extraData ) public view returns (bool) { BattleContext memory ctx = ENGINE.getBattleContext(battleKey); uint256 activeMonIndex = (playerIndex == 0) ? ctx.p0ActiveMonIndex : ctx.p1ActiveMonIndex; @@ -127,7 +127,7 @@ contract DefaultValidator is IValidator { } // Validates that you can't switch to the same mon, you have enough stamina, the move isn't disabled, etc. - function validatePlayerMove(bytes32 battleKey, uint256 moveIndex, uint256 playerIndex, uint240 extraData) + function validatePlayerMove(bytes32 battleKey, uint256 moveIndex, uint256 playerIndex, uint16 extraData) external view returns (bool) @@ -191,7 +191,7 @@ contract DefaultValidator is IValidator { bytes32 battleKey, uint256 moveIndex, uint256 playerIndex, - uint240 extraData, + uint16 extraData, uint256 activeMonIndex, ValidationContext memory vctx ) internal view returns (bool) { diff --git a/src/Engine.sol b/src/Engine.sol index 4f608183..4a42d10d 100644 --- a/src/Engine.sol +++ b/src/Engine.sol @@ -46,8 +46,8 @@ contract Engine is IEngine, MappingAllocator { // A non-zero encoded move is the "transient is populated for this call" signal. uint256 private transient _turnP0MoveEncoded; uint256 private transient _turnP1MoveEncoded; - bytes32 private transient _turnP0Salt; - bytes32 private transient _turnP1Salt; + uint104 private transient _turnP0Salt; + uint104 private transient _turnP1Salt; // Errors error NoWriteAllowed(); @@ -65,7 +65,7 @@ contract Engine is IEngine, MappingAllocator { // Events event BattleStart(bytes32 indexed battleKey, address p0, address p1); event MonMove( - bytes32 indexed battleKey, uint256 packedPlayerIndexMonIndex, uint256 packedMoveIndexExtraData, bytes32 salt + bytes32 indexed battleKey, uint256 packedPlayerIndexMonIndex, uint256 packedMoveIndexExtraData, uint104 salt ); event EngineExecute(bytes32 indexed battleKey); event BattleComplete(bytes32 indexed battleKey, address winner); @@ -300,11 +300,11 @@ contract Engine is IEngine, MappingAllocator { function executeWithMoves( bytes32 battleKey, uint8 p0MoveIndex, - bytes32 p0Salt, - uint240 p0ExtraData, + uint104 p0Salt, + uint16 p0ExtraData, uint8 p1MoveIndex, - bytes32 p1Salt, - uint240 p1ExtraData + uint104 p1Salt, + uint16 p1ExtraData ) external { bytes32 storageKey = _getStorageKey(battleKey); storageKeyForWrite = storageKey; @@ -329,7 +329,7 @@ contract Engine is IEngine, MappingAllocator { /// @notice Combined single-player setMove + execute for forced switch turns /// @dev Only callable by moveManager. The acting player is inferred from battle.playerSwitchForTurnFlag. - function executeWithSingleMove(bytes32 battleKey, uint8 moveIndex, bytes32 salt, uint240 extraData) external { + function executeWithSingleMove(bytes32 battleKey, uint8 moveIndex, uint104 salt, uint16 extraData) external { bytes32 storageKey = _getStorageKey(battleKey); storageKeyForWrite = storageKey; @@ -358,12 +358,12 @@ contract Engine is IEngine, MappingAllocator { _executeInternal(battleKey, storageKey); } - /// @dev Decodes a transient-encoded move (layout: [extraData:240 | packedMoveIndex:8]) into a + /// @dev Decodes a transient-encoded move (layout: [extraData:16 | packedMoveIndex:8]) into a /// MoveDecision. Encoded == 0 means "no current turn move" since packedMoveIndex always has /// IS_REAL_TURN_BIT set for a real move. function _decodeMove(uint256 encoded) private pure returns (MoveDecision memory m) { m.packedMoveIndex = uint8(encoded & 0xFF); - m.extraData = uint240(encoded >> 8); + m.extraData = uint16(encoded >> 8); } /// @dev Returns the current turn's MoveDecision for `playerIndex`. During an active @@ -381,7 +381,7 @@ contract Engine is IEngine, MappingAllocator { } /// @dev Salt companion to `_getCurrentTurnMove`. - function _getCurrentTurnSalt(BattleConfig storage config, uint256 playerIndex) internal view returns (bytes32) { + function _getCurrentTurnSalt(BattleConfig storage config, uint256 playerIndex) internal view returns (uint104) { uint256 encoded = playerIndex == 0 ? _turnP0MoveEncoded : _turnP1MoveEncoded; if (encoded != 0) { return playerIndex == 0 ? _turnP0Salt : _turnP1Salt; @@ -481,12 +481,12 @@ contract Engine is IEngine, MappingAllocator { // Update the temporary RNG to the newest value // Inline RNG computation when oracle is address(0) to avoid external call uint256 rng; - bytes32 p0TurnSalt = _getCurrentTurnSalt(config, 0); - bytes32 p1TurnSalt = _getCurrentTurnSalt(config, 1); + uint104 p0TurnSalt = _getCurrentTurnSalt(config, 0); + uint104 p1TurnSalt = _getCurrentTurnSalt(config, 1); if (address(config.rngOracle) == address(0)) { rng = uint256(keccak256(abi.encode(p0TurnSalt, p1TurnSalt))); } else { - rng = config.rngOracle.getRNG(p0TurnSalt, p1TurnSalt); + rng = config.rngOracle.getRNG(bytes32(uint256(p0TurnSalt)), bytes32(uint256(p1TurnSalt))); } tempRNG = rng; @@ -730,8 +730,8 @@ contract Engine is IEngine, MappingAllocator { function resetCallContext() external { _turnP0MoveEncoded = 0; _turnP1MoveEncoded = 0; - _turnP0Salt = bytes32(0); - _turnP1Salt = bytes32(0); + _turnP0Salt = 0; + _turnP1Salt = 0; } function end(bytes32 battleKey) external { @@ -1377,8 +1377,8 @@ contract Engine is IEngine, MappingAllocator { BattleConfig storage config, uint256 playerIndex, uint8 moveIndex, - bytes32 salt, - uint240 extraData + uint104 salt, + uint16 extraData ) internal { // Pack moveIndex with isRealTurn bit and apply +1 offset for regular moves // Regular moves (< SWITCH_MOVE_INDEX) are stored as moveIndex + 1 to avoid zero ambiguity @@ -1395,7 +1395,7 @@ contract Engine is IEngine, MappingAllocator { } } - function setMove(bytes32 battleKey, uint256 playerIndex, uint8 moveIndex, bytes32 salt, uint240 extraData) + function setMove(bytes32 battleKey, uint256 playerIndex, uint8 moveIndex, uint104 salt, uint16 extraData) external { bool isInsideExecute = _turnP0MoveEncoded != 0 || _turnP1MoveEncoded != 0; @@ -1565,7 +1565,7 @@ contract Engine is IEngine, MappingAllocator { // check below silently no-ops and timeout handles the stuck player. if ((battle.turnId == 0 || currentMonState.isKnockedOut) && moveIndex != SWITCH_MOVE_INDEX) { moveIndex = SWITCH_MOVE_INDEX; - move.extraData = uint240(0); + move.extraData = uint16(0); } // Handle a switch, no-op, or regular move. @@ -2391,7 +2391,7 @@ contract Engine is IEngine, MappingAllocator { /// @notice Validates a player move, handling both inline validation (when validator is address(0)) and external validators /// @dev This allows callers like CPU to validate moves without needing to handle the address(0) case themselves - function validatePlayerMoveForBattle(bytes32 battleKey, uint256 moveIndex, uint256 playerIndex, uint240 extraData) + function validatePlayerMoveForBattle(bytes32 battleKey, uint256 moveIndex, uint256 playerIndex, uint16 extraData) external returns (bool) { diff --git a/src/IEngine.sol b/src/IEngine.sol index 3dd7273f..a75b3e1d 100644 --- a/src/IEngine.sol +++ b/src/IEngine.sol @@ -37,18 +37,18 @@ interface IEngine { uint256 rng ) external returns (int32 damage, bytes32 eventType); function switchActiveMon(uint256 playerIndex, uint256 monToSwitchIndex) external; - function setMove(bytes32 battleKey, uint256 playerIndex, uint8 moveIndex, bytes32 salt, uint240 extraData) external; + function setMove(bytes32 battleKey, uint256 playerIndex, uint8 moveIndex, uint104 salt, uint16 extraData) external; function execute(bytes32 battleKey) external; function executeWithMoves( bytes32 battleKey, uint8 p0MoveIndex, - bytes32 p0Salt, - uint240 p0ExtraData, + uint104 p0Salt, + uint16 p0ExtraData, uint8 p1MoveIndex, - bytes32 p1Salt, - uint240 p1ExtraData + uint104 p1Salt, + uint16 p1ExtraData ) external; - function executeWithSingleMove(bytes32 battleKey, uint8 moveIndex, bytes32 salt, uint240 extraData) external; + function executeWithSingleMove(bytes32 battleKey, uint8 moveIndex, uint104 salt, uint16 extraData) external; function resetCallContext() external; // Getters @@ -94,7 +94,7 @@ interface IEngine { function getPlayerSwitchForTurnFlagForBattleState(bytes32 battleKey) external view returns (uint256); function getGlobalKV(bytes32 battleKey, uint64 key) external view returns (uint192); function getBattleValidator(bytes32 battleKey) external view returns (IValidator); - function validatePlayerMoveForBattle(bytes32 battleKey, uint256 moveIndex, uint256 playerIndex, uint240 extraData) + function validatePlayerMoveForBattle(bytes32 battleKey, uint256 moveIndex, uint256 playerIndex, uint16 extraData) external returns (bool); function getEffects(bytes32 battleKey, uint256 targetIndex, uint256 monIndex) diff --git a/src/IValidator.sol b/src/IValidator.sol index 8dea5f1d..2f71e565 100644 --- a/src/IValidator.sol +++ b/src/IValidator.sol @@ -11,7 +11,7 @@ interface IValidator { ) external returns (bool); // Validates that you can't switch to the same mon, you have enough stamina, the move isn't disabled, etc. - function validatePlayerMove(bytes32 battleKey, uint256 moveIndex, uint256 playerIndex, uint240 extraData) + function validatePlayerMove(bytes32 battleKey, uint256 moveIndex, uint256 playerIndex, uint16 extraData) external returns (bool); @@ -20,7 +20,7 @@ interface IValidator { bytes32 battleKey, uint256 moveIndex, uint256 playerIndex, - uint240 extraData + uint16 extraData ) external returns (bool); // Validates that a switch is valid diff --git a/src/Structs.sol b/src/Structs.sol index 327a52ec..0504a330 100644 --- a/src/Structs.sol +++ b/src/Structs.sol @@ -47,11 +47,11 @@ struct Battle { IEngineHook[] engineHooks; } -// Packed into 1 storage slot (8 + 240 = 248 bits) +// Packed into 1 storage slot (8 + 16 = 24 bits) // packedMoveIndex: lower 7 bits = moveIndex (0-127), bit 7 = isRealTurn (1 = real, 0 = not set) struct MoveDecision { uint8 packedMoveIndex; - uint240 extraData; + uint16 extraData; } // Stored by the Engine, tracks immutable battle data and battle state @@ -81,8 +81,8 @@ struct BattleConfig { uint40 startTimestamp; // 40 — battle start time; overflows in year ~36825 (shrunk from uint48 for slot-2 packing) bool hasInlineStaminaRegen; // 8 uint8 globalKVCount; // 8 — live entry count in the current battle's globalKV key buffer - bytes32 p0Salt; - bytes32 p1Salt; + uint104 p0Salt; + uint104 p1Salt; MoveDecision p0Move; MoveDecision p1Move; mapping(uint256 index => Mon) p0Team; @@ -118,8 +118,8 @@ struct BattleConfigView { uint96 packedP1EffectsCount; uint8 teamSizes; uint40 startTimestamp; // Needed client-side for the getGlobalKV freshness gate - bytes32 p0Salt; - bytes32 p1Salt; + uint104 p0Salt; + uint104 p1Salt; MoveDecision p0Move; MoveDecision p1Move; EffectInstance[] globalEffects; @@ -185,8 +185,8 @@ struct PlayerDecisionData { struct RevealedMove { uint8 moveIndex; - uint240 extraData; - bytes32 salt; + uint16 extraData; + uint104 salt; } // Used for StatBoosts diff --git a/src/commit-manager/DefaultCommitManager.sol b/src/commit-manager/DefaultCommitManager.sol index e2190275..d681cbeb 100644 --- a/src/commit-manager/DefaultCommitManager.sol +++ b/src/commit-manager/DefaultCommitManager.sol @@ -118,7 +118,7 @@ contract DefaultCommitManager is ICommitManager { emit MoveCommit(battleKey, caller); } - function revealMove(bytes32 battleKey, uint8 moveIndex, bytes32 salt, uint240 extraData, bool autoExecute) + function revealMove(bytes32 battleKey, uint8 moveIndex, uint104 salt, uint16 extraData, bool autoExecute) external { // Get all battle context in one call diff --git a/src/commit-manager/ICommitManager.sol b/src/commit-manager/ICommitManager.sol index f9e25bb5..f666c1d8 100644 --- a/src/commit-manager/ICommitManager.sol +++ b/src/commit-manager/ICommitManager.sol @@ -5,7 +5,7 @@ import "../Structs.sol"; interface ICommitManager { function commitMove(bytes32 battleKey, bytes32 moveHash) external; - function revealMove(bytes32 battleKey, uint8 moveIndex, bytes32 salt, uint240 extraData, bool autoExecute) + function revealMove(bytes32 battleKey, uint8 moveIndex, uint104 salt, uint16 extraData, bool autoExecute) external; function getCommitment(bytes32 battleKey, address player) external view returns (bytes32 moveHash, uint256 turnId); function getMoveCountForBattleState(bytes32 battleKey, address player) external view returns (uint256); diff --git a/src/commit-manager/SignedCommitLib.sol b/src/commit-manager/SignedCommitLib.sol index 4646d4ee..3342a749 100644 --- a/src/commit-manager/SignedCommitLib.sol +++ b/src/commit-manager/SignedCommitLib.sol @@ -40,14 +40,14 @@ library SignedCommitLib { uint64 turnId; bytes32 committerMoveHash; // A's hash that B signs over uint8 revealerMoveIndex; - bytes32 revealerSalt; - uint240 revealerExtraData; + uint104 revealerSalt; + uint16 revealerExtraData; } /// @notice Computes the type hash for DualSignedReveal function computeDualSignedRevealTypehash() internal pure returns (bytes32) { return keccak256( - "DualSignedReveal(bytes32 battleKey,uint64 turnId,bytes32 committerMoveHash,uint8 revealerMoveIndex,bytes32 revealerSalt,uint240 revealerExtraData)" + "DualSignedReveal(bytes32 battleKey,uint64 turnId,bytes32 committerMoveHash,uint8 revealerMoveIndex,uint104 revealerSalt,uint16 revealerExtraData)" ); } @@ -58,7 +58,7 @@ library SignedCommitLib { return keccak256( abi.encode( keccak256( - "DualSignedReveal(bytes32 battleKey,uint64 turnId,bytes32 committerMoveHash,uint8 revealerMoveIndex,bytes32 revealerSalt,uint240 revealerExtraData)" + "DualSignedReveal(bytes32 battleKey,uint64 turnId,bytes32 committerMoveHash,uint8 revealerMoveIndex,uint104 revealerSalt,uint16 revealerExtraData)" ), reveal.battleKey, reveal.turnId, diff --git a/src/commit-manager/SignedCommitManager.sol b/src/commit-manager/SignedCommitManager.sol index 20bd4aaf..aa0831fa 100644 --- a/src/commit-manager/SignedCommitManager.sol +++ b/src/commit-manager/SignedCommitManager.sol @@ -20,13 +20,16 @@ import {SignedCommitLib} from "./SignedCommitLib.sol"; /// 3. Alice reveals (TX 3) /// /// Dual-signed flow (1 transaction): -/// 1. Alice signs her move hash off-chain, sends to Bob -/// 2. Bob signs his move + Alice's hash off-chain, sends to Alice -/// 3. Alice calls executeWithDualSignedMoves with both moves + Bob's signature (TX 1) +/// 1. Alice signs her move hash off-chain (SignedCommit), sends to Bob +/// 2. Bob signs his move + Alice's hash off-chain (DualSignedReveal), sends back +/// 3. Anyone (Alice, Bob, or a relayer) calls executeWithDualSignedMoves with +/// both signatures + Alice's preimage (TX 1) /// -/// Security: Alice commits to her hash before seeing Bob's move. Bob signs over -/// Alice's hash, so even though Alice sees Bob's move before submitting, Alice -/// cannot change her move (must provide valid preimage for the hash Bob signed). +/// Security: Alice commits to her hash before seeing Bob's move (binding Alice +/// cryptographically via her SignedCommit). Bob signs over Alice's hash (binding +/// Bob via his DualSignedReveal). Both signatures together prove both players' +/// intent without trusting msg.sender — submission can be relayed without +/// reopening any unilateral-revealer attack. /// /// Fallback if Alice stalls: Bob can use commitWithSignature() to publish Alice's /// signed commitment on-chain, then continue with the normal reveal flow. @@ -36,9 +39,6 @@ contract SignedCommitManager is DefaultCommitManager, EIP712 { /// @notice Thrown when the signature verification fails error InvalidSignature(); - /// @notice Thrown when caller is not the committing player for this turn - error CallerNotCommitter(); - /// @notice Thrown when trying to use dual-signed flow on a single-player turn error NotTwoPlayerTurn(); @@ -54,8 +54,12 @@ contract SignedCommitManager is DefaultCommitManager, EIP712 { } /// @notice Executes a turn using dual-signed moves from both players (gas-optimized) - /// @dev The committer (A) submits both moves. The revealer (B) has signed over - /// their move and A's move hash, binding both players to their moves. + /// @dev Both players sign off-chain — committer over `SignedCommit{committerMoveHash, …}` + /// and revealer over `DualSignedReveal{committerMoveHash, …, revealerMove…}`. Anyone + /// can submit (relayer-friendly) since both signatures are required and bind each + /// player independently. Without the explicit committer signature, a malicious + /// revealer could pick any preimage `P*`, sign `DualSignedReveal{keccak(P*), …}` + /// and play `P*` as the committer's move — the committer signature closes that. /// @param battleKey The battle identifier /// @param committerMoveIndex The committer's move index /// @param committerSalt The committer's salt @@ -63,48 +67,54 @@ contract SignedCommitManager is DefaultCommitManager, EIP712 { /// @param revealerMoveIndex The revealer's move index /// @param revealerSalt The revealer's salt /// @param revealerExtraData The revealer's extra data + /// @param committerSignature EIP-712 signature from the committer over + /// SignedCommit(committerMoveHash, battleKey, turnId) /// @param revealerSignature EIP-712 signature from the revealer over - /// DualSignedReveal(battleKey, turnId, committerMoveHash, revealerMove...) + /// DualSignedReveal(battleKey, turnId, committerMoveHash, revealerMove…) function executeWithDualSignedMoves( bytes32 battleKey, uint8 committerMoveIndex, - bytes32 committerSalt, - uint240 committerExtraData, + uint104 committerSalt, + uint16 committerExtraData, uint8 revealerMoveIndex, - bytes32 revealerSalt, - uint240 revealerExtraData, + uint104 revealerSalt, + uint16 revealerExtraData, + bytes calldata committerSignature, bytes calldata revealerSignature ) external { - // Use lightweight getter (validates internally, reverts on bad state) (address committer, address revealer, uint64 turnId) = ENGINE.getCommitAuthForDualSigned(battleKey); - // Caller must be the committing player - if (msg.sender != committer) { - revert CallerNotCommitter(); - } - - // Compute the committer's move hash bytes32 committerMoveHash = keccak256(abi.encodePacked(committerMoveIndex, committerSalt, committerExtraData)); - // Verify the revealer's signature over DualSignedReveal - SignedCommitLib.DualSignedReveal memory reveal = SignedCommitLib.DualSignedReveal({ - battleKey: battleKey, - turnId: turnId, - committerMoveHash: committerMoveHash, - revealerMoveIndex: revealerMoveIndex, - revealerSalt: revealerSalt, - revealerExtraData: revealerExtraData - }); - - bytes32 digest = _hashTypedData(SignedCommitLib.hashDualSignedReveal(reveal)); - if (ECDSA.recoverCalldata(digest, revealerSignature) != revealer) { - revert InvalidSignature(); + // Scoped to keep `commit`/`reveal` structs from sharing stack space across recoveries. + { + SignedCommitLib.SignedCommit memory commit = SignedCommitLib.SignedCommit({ + moveHash: committerMoveHash, + battleKey: battleKey, + turnId: turnId + }); + bytes32 commitDigest = _hashTypedData(SignedCommitLib.hashSignedCommit(commit)); + if (ECDSA.recoverCalldata(commitDigest, committerSignature) != committer) { + revert InvalidSignature(); + } + } + + { + SignedCommitLib.DualSignedReveal memory reveal = SignedCommitLib.DualSignedReveal({ + battleKey: battleKey, + turnId: turnId, + committerMoveHash: committerMoveHash, + revealerMoveIndex: revealerMoveIndex, + revealerSalt: revealerSalt, + revealerExtraData: revealerExtraData + }); + bytes32 revealDigest = _hashTypedData(SignedCommitLib.hashDualSignedReveal(reveal)); + if (ECDSA.recoverCalldata(revealDigest, revealerSignature) != revealer) { + revert InvalidSignature(); + } } - // Execute with moves in a single call (engine validates during execution) - // No playerData updates needed - engine tracks lastExecuteTimestamp for timeouts if (turnId % 2 == 0) { - // Committer is p0 ENGINE.executeWithMoves( battleKey, committerMoveIndex, @@ -115,7 +125,6 @@ contract SignedCommitManager is DefaultCommitManager, EIP712 { revealerExtraData ); } else { - // Committer is p1 ENGINE.executeWithMoves( battleKey, revealerMoveIndex, @@ -130,7 +139,7 @@ contract SignedCommitManager is DefaultCommitManager, EIP712 { /// @notice Executes a forced single-player move, usually a switch after a KO, in one transaction. /// @dev The acting player is inferred from the engine's switch flag and must be msg.sender. - function executeSinglePlayerMove(bytes32 battleKey, uint8 moveIndex, bytes32 salt, uint240 extraData) external { + function executeSinglePlayerMove(bytes32 battleKey, uint8 moveIndex, uint104 salt, uint16 extraData) external { CommitContext memory ctx = ENGINE.getCommitContext(battleKey); if (ctx.startTimestamp == 0) { diff --git a/src/cpu/BetterCPU.sol b/src/cpu/BetterCPU.sol index 68077cc9..4a1cbec0 100644 --- a/src/cpu/BetterCPU.sol +++ b/src/cpu/BetterCPU.sol @@ -56,10 +56,10 @@ contract BetterCPU is CPU { // ============ CORE DECISION TREE ============ - function calculateMove(CPUContext memory ctx, uint8 playerMoveIndex, uint240 playerExtraData) + function calculateMove(CPUContext memory ctx, uint8 playerMoveIndex, uint16 playerExtraData) external override - returns (uint128 moveIndex, uint240 extraData) + returns (uint128 moveIndex, uint16 extraData) { (RevealedMove[] memory noOp, RevealedMove[] memory moves, RevealedMove[] memory switches) = _calculateValidMoves(ctx); @@ -380,9 +380,9 @@ contract BetterCPU is CPU { // ============ LEAD SELECTION ============ /// @notice Select lead with dual-type scoring (defensive + offensive) - function _selectLead(bytes32 battleKey, uint240 opponentMonExtraData, RevealedMove[] memory switches) + function _selectLead(bytes32 battleKey, uint16 opponentMonExtraData, RevealedMove[] memory switches) internal - returns (uint128, uint240) + returns (uint128, uint16) { MonStats memory oppStats = ENGINE.getMonStatsForBattle(battleKey, 0, uint256(opponentMonExtraData)); Type oppType1 = oppStats.type1; @@ -445,7 +445,7 @@ contract BetterCPU is CPU { uint256 opponentMonIndex, uint8 opponentMoveIndex, RevealedMove[] memory switches - ) internal view returns (uint128, uint240) { + ) internal view returns (uint128, uint16) { // If opponent isn't attacking, fall back to random if (opponentMoveIndex >= SWITCH_MOVE_INDEX) { return (switches[0].moveIndex, switches[0].extraData); diff --git a/src/cpu/CPU.sol b/src/cpu/CPU.sol index e5553cc8..b48b1a93 100644 --- a/src/cpu/CPU.sol +++ b/src/cpu/CPU.sol @@ -34,10 +34,10 @@ abstract contract CPU is CPUMoveManager, ICPU, ICPURNG, IMatchmaker { * If it's turn 0, randomly selects a mon index to swap to * Otherwise, randomly selects a valid move, switch index, or no op */ - function calculateMove(CPUContext memory ctx, uint8 playerMoveIndex, uint240 playerExtraData) + function calculateMove(CPUContext memory ctx, uint8 playerMoveIndex, uint16 playerExtraData) external virtual - returns (uint128 moveIndex, uint240 extraData); + returns (uint128 moveIndex, uint16 extraData); /** * Public test-friendly wrapper: fetches context and forwards. playerIndex is ignored @@ -63,7 +63,7 @@ abstract contract CPU is CPUMoveManager, ICPU, ICPURNG, IMatchmaker { uint256 teamSize = ctx.p1TeamSize; RevealedMove[] memory switchChoices = new RevealedMove[](teamSize); for (uint256 i = 0; i < teamSize; i++) { - switchChoices[i] = RevealedMove({moveIndex: SWITCH_MOVE_INDEX, salt: "", extraData: uint240(i)}); + switchChoices[i] = RevealedMove({moveIndex: SWITCH_MOVE_INDEX, salt: 0, extraData: uint16(i)}); } return (new RevealedMove[](0), new RevealedMove[](0), switchChoices); } @@ -77,7 +77,7 @@ abstract contract CPU is CPUMoveManager, ICPU, ICPURNG, IMatchmaker { validSwitchIndices = new uint256[](teamSize); for (uint256 i = 0; i < teamSize; i++) { if (i != activeMonIndex) { - if (_validateCPUMove(ctx, SWITCH_MOVE_INDEX, uint240(i))) { + if (_validateCPUMove(ctx, SWITCH_MOVE_INDEX, uint16(i))) { validSwitchIndices[validSwitchCount++] = i; } } @@ -89,19 +89,19 @@ abstract contract CPU is CPUMoveManager, ICPU, ICPURNG, IMatchmaker { for (uint256 i = 0; i < validSwitchCount; i++) { switchChoices[i] = RevealedMove({ moveIndex: SWITCH_MOVE_INDEX, - salt: "", - extraData: uint240(validSwitchIndices[i]) + salt: 0, + extraData: uint16(validSwitchIndices[i]) }); } return (new RevealedMove[](0), new RevealedMove[](0), switchChoices); } uint8[] memory validMoveIndices = new uint8[](NUM_MOVES); - uint240[] memory validMoveExtraData = new uint240[](NUM_MOVES); + uint16[] memory validMoveExtraData = new uint16[](NUM_MOVES); uint256 validMoveCount; // Check for valid moves for (uint256 i = 0; i < NUM_MOVES; i++) { uint256 rawMoveSlot = ctx.cpuActiveMonMoveSlots[i]; - uint240 extraDataToUse = 0; + uint16 extraDataToUse = 0; // Inline moves always have ExtraDataType.None — skip extraData logic if (!MoveSlotLib.isInline(rawMoveSlot)) { IMoveSet move = MoveSlotLib.toIMoveSet(rawMoveSlot); @@ -112,7 +112,7 @@ abstract contract CPU is CPUMoveManager, ICPU, ICPURNG, IMatchmaker { } uint256 randomIndex = _sampleRNG(keccak256(abi.encode(nonce++, ctx.battleKey, block.timestamp))) % validSwitchCount; - extraDataToUse = uint240(validSwitchIndices[randomIndex]); + extraDataToUse = uint16(validSwitchIndices[randomIndex]); validMoveExtraData[validMoveCount] = extraDataToUse; } else if (move.extraDataType() == ExtraDataType.OpponentNonKOTeamIndex) { uint256 opponentTeamSize = ctx.p0TeamSize; @@ -129,7 +129,7 @@ abstract contract CPU is CPUMoveManager, ICPU, ICPURNG, IMatchmaker { } uint256 randomIndex = _sampleRNG(keccak256(abi.encode(nonce++, ctx.battleKey, block.timestamp))) % validTargetCount; - extraDataToUse = uint240(validTargets[randomIndex]); + extraDataToUse = uint16(validTargets[randomIndex]); validMoveExtraData[validMoveCount] = extraDataToUse; } } @@ -141,15 +141,15 @@ abstract contract CPU is CPUMoveManager, ICPU, ICPURNG, IMatchmaker { RevealedMove[] memory validMovesArray = new RevealedMove[](validMoveCount); for (uint256 i = 0; i < validMoveCount; i++) { validMovesArray[i] = - RevealedMove({moveIndex: validMoveIndices[i], salt: "", extraData: validMoveExtraData[i]}); + RevealedMove({moveIndex: validMoveIndices[i], salt: 0, extraData: validMoveExtraData[i]}); } RevealedMove[] memory validSwitchesArray = new RevealedMove[](validSwitchCount); for (uint256 i = 0; i < validSwitchCount; i++) { validSwitchesArray[i] = - RevealedMove({moveIndex: SWITCH_MOVE_INDEX, salt: "", extraData: uint240(validSwitchIndices[i])}); + RevealedMove({moveIndex: SWITCH_MOVE_INDEX, salt: 0, extraData: uint16(validSwitchIndices[i])}); } RevealedMove[] memory noOpArray = new RevealedMove[](1); - noOpArray[0] = RevealedMove({moveIndex: NO_OP_MOVE_INDEX, salt: "", extraData: 0}); + noOpArray[0] = RevealedMove({moveIndex: NO_OP_MOVE_INDEX, salt: 0, extraData: 0}); nonceToUse = nonce; return (noOpArray, validMovesArray, validSwitchesArray); @@ -172,7 +172,7 @@ abstract contract CPU is CPUMoveManager, ICPU, ICPURNG, IMatchmaker { /// DEFAULT_MOVES_PER_MON / DEFAULT_MONS_PER_TEAM; the CPU's own iteration already bounds /// moveIndex below NUM_MOVES and monToSwitchIndex below p1TeamSize, so the basic/switch /// checks are equivalent to the engine-side versions. - function _validateCPUMove(CPUContext memory ctx, uint8 moveIndex, uint240 extraData) + function _validateCPUMove(CPUContext memory ctx, uint8 moveIndex, uint16 extraData) internal returns (bool) { @@ -182,7 +182,7 @@ abstract contract CPU is CPUMoveManager, ICPU, ICPURNG, IMatchmaker { return ENGINE.validatePlayerMoveForBattle(ctx.battleKey, moveIndex, 1, extraData); } - function _inlineValidateCPUMove(CPUContext memory ctx, uint8 moveIndex, uint240 extraData) + function _inlineValidateCPUMove(CPUContext memory ctx, uint8 moveIndex, uint16 extraData) private view returns (bool) diff --git a/src/cpu/CPUMoveManager.sol b/src/cpu/CPUMoveManager.sol index 734a03e6..1f286113 100644 --- a/src/cpu/CPUMoveManager.sol +++ b/src/cpu/CPUMoveManager.sol @@ -22,7 +22,7 @@ abstract contract CPUMoveManager { engine.updateMatchmakers(self, empty); } - function selectMove(bytes32 battleKey, uint8 moveIndex, bytes32 salt, uint240 extraData) external { + function selectMove(bytes32 battleKey, uint8 moveIndex, uint104 salt, uint16 extraData) external { // Cheap routing staticcall: one SLOAD for p0 / winnerIndex / playerSwitchForTurnFlag. // When the turn is "p0 forced switch" (flag == 0) or the game is already over we return // without ever paying for the full CPUContext (which would load team sizes, KO bitmaps, @@ -44,8 +44,10 @@ abstract contract CPUMoveManager { // P1's turn or both players move: CPU calculates its move. Fetch the full context now. CPUContext memory ctx = ENGINE.getCPUContext(battleKey); - (uint128 cpuMoveIndex, uint240 cpuExtraData) = ICPU(address(this)).calculateMove(ctx, moveIndex, extraData); - bytes32 p1Salt = keccak256(abi.encode(battleKey, msg.sender, block.timestamp)); + (uint128 cpuMoveIndex, uint16 cpuExtraData) = ICPU(address(this)).calculateMove(ctx, moveIndex, extraData); + // Salt narrows to 104 bits to match the engine's storage; ample for an unpredictable + // RNG source within the seconds-to-minutes commit-reveal window. + uint104 p1Salt = uint104(uint256(keccak256(abi.encode(battleKey, msg.sender, block.timestamp)))); if (playerSwitchForTurnFlag == 1) { ENGINE.executeWithSingleMove(battleKey, uint8(cpuMoveIndex), p1Salt, cpuExtraData); diff --git a/src/cpu/ICPU.sol b/src/cpu/ICPU.sol index 01e1b381..49e6c1c3 100644 --- a/src/cpu/ICPU.sol +++ b/src/cpu/ICPU.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.0; import {CPUContext, ProposedBattle} from "../Structs.sol"; interface ICPU { - function calculateMove(CPUContext memory ctx, uint8 playerMoveIndex, uint240 playerExtraData) + function calculateMove(CPUContext memory ctx, uint8 playerMoveIndex, uint16 playerExtraData) external - returns (uint128 moveIndex, uint240 extraData); + returns (uint128 moveIndex, uint16 extraData); function startBattle(ProposedBattle memory proposal) external returns (bytes32 battleKey); } diff --git a/src/cpu/OkayCPU.sol b/src/cpu/OkayCPU.sol index 0b38edd4..c5eccddb 100644 --- a/src/cpu/OkayCPU.sol +++ b/src/cpu/OkayCPU.sol @@ -21,10 +21,10 @@ contract OkayCPU is CPU { /** * If it's turn 0, swap in a mon that resists the other player's type1 (if possible) */ - function calculateMove(CPUContext memory ctx, uint8, uint240) + function calculateMove(CPUContext memory ctx, uint8, uint16) external override - returns (uint128 moveIndex, uint240 extraData) + returns (uint128 moveIndex, uint16 extraData) { (RevealedMove[] memory noOp, RevealedMove[] memory moves, RevealedMove[] memory switches) = _calculateValidMoves(ctx); @@ -126,7 +126,7 @@ contract OkayCPU is CPU { RevealedMove[] memory noOp, RevealedMove[] memory moves, RevealedMove[] memory switches - ) internal returns (uint128, uint240) { + ) internal returns (uint128, uint16) { // Single RNG call - use different bit ranges for different random decisions uint256 rng = _getRNG(battleKey); uint256 movesLen = moves.length; diff --git a/src/cpu/PlayerCPU.sol b/src/cpu/PlayerCPU.sol index 9cb710e4..7c0712e4 100644 --- a/src/cpu/PlayerCPU.sol +++ b/src/cpu/PlayerCPU.sol @@ -13,21 +13,21 @@ contract PlayerCPU is CPU { constructor(uint256 numMoves, IEngine engine, ICPURNG rng) CPU(numMoves, engine, rng) {} - function setMove(bytes32 battleKey, uint8 moveIndex, uint240 extraData) external { + function setMove(bytes32 battleKey, uint8 moveIndex, uint16 extraData) external { if (msg.sender != ENGINE.getPlayersForBattle(battleKey)[0]) { revert NotP0(); } - declaredMoveForBattle[battleKey] = RevealedMove({moveIndex: moveIndex, salt: "", extraData: extraData}); + declaredMoveForBattle[battleKey] = RevealedMove({moveIndex: moveIndex, salt: 0, extraData: extraData}); } /** * Returns the move that p0 declared for this CPU, ignoring the rest of the context. */ - function calculateMove(CPUContext memory ctx, uint8, uint240) + function calculateMove(CPUContext memory ctx, uint8, uint16) external view override - returns (uint128 moveIndex, uint240 extraData) + returns (uint128 moveIndex, uint16 extraData) { return (declaredMoveForBattle[ctx.battleKey].moveIndex, declaredMoveForBattle[ctx.battleKey].extraData); } diff --git a/src/cpu/RandomCPU.sol b/src/cpu/RandomCPU.sol index 83f37d4c..0627d40d 100644 --- a/src/cpu/RandomCPU.sol +++ b/src/cpu/RandomCPU.sol @@ -15,10 +15,10 @@ contract RandomCPU is CPU { * If it's turn 0, randomly selects a mon index to swap to * Otherwise, randomly selects a valid move, switch index, or no op */ - function calculateMove(CPUContext memory ctx, uint8, uint240) + function calculateMove(CPUContext memory ctx, uint8, uint16) external override - returns (uint128 moveIndex, uint240 extraData) + returns (uint128 moveIndex, uint16 extraData) { (RevealedMove[] memory noOp, RevealedMove[] memory moves, RevealedMove[] memory switches) = _calculateValidMoves(ctx); diff --git a/src/effects/status/SleepStatus.sol b/src/effects/status/SleepStatus.sol index 564ae40a..a7e679da 100644 --- a/src/effects/status/SleepStatus.sol +++ b/src/effects/status/SleepStatus.sol @@ -41,7 +41,7 @@ contract SleepStatus is StatusEffect { MoveDecision memory moveDecision = engine.getMoveDecisionForBattleState(battleKey, targetIndex); uint8 moveIndex = moveDecision.packedMoveIndex & MOVE_INDEX_MASK; if (moveIndex != SWITCH_MOVE_INDEX) { - engine.setMove(battleKey, targetIndex, NO_OP_MOVE_INDEX, "", 0); + engine.setMove(battleKey, targetIndex, NO_OP_MOVE_INDEX, 0, 0); } } diff --git a/src/lib/ValidatorLogic.sol b/src/lib/ValidatorLogic.sol index 172229fb..060d9be2 100644 --- a/src/lib/ValidatorLogic.sol +++ b/src/lib/ValidatorLogic.sol @@ -43,7 +43,7 @@ library ValidatorLogic { uint256 rawMoveSlot, uint256 playerIndex, uint256 activeMonIndex, - uint240 extraData, + uint16 extraData, uint32 baseStamina, int32 staminaDelta ) internal view returns (bool valid) { diff --git a/src/mons/aurox/BullRush.sol b/src/mons/aurox/BullRush.sol index 7902cee2..21e747e6 100644 --- a/src/mons/aurox/BullRush.sol +++ b/src/mons/aurox/BullRush.sol @@ -40,7 +40,7 @@ contract BullRush is StandardAttack { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256 defenderMonIndex, - uint240, + uint16, uint256 rng ) public override { // Deal the damage to opponent diff --git a/src/mons/aurox/GildedRecovery.sol b/src/mons/aurox/GildedRecovery.sol index be90436a..66b939c1 100644 --- a/src/mons/aurox/GildedRecovery.sol +++ b/src/mons/aurox/GildedRecovery.sol @@ -24,10 +24,10 @@ contract GildedRecovery is IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256, - uint240 extraData, + uint16 extraData, uint256 ) external { - // extraData contains the mon index as raw uint240 + // extraData contains the mon index as raw uint16 uint256 targetMonIndex = uint256(extraData); // Check if the target mon has a status effect @@ -84,7 +84,7 @@ contract GildedRecovery is IMoveSet { return MoveClass.Self; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/aurox/IronWall.sol b/src/mons/aurox/IronWall.sol index 397d3d6c..3c8d807f 100644 --- a/src/mons/aurox/IronWall.sol +++ b/src/mons/aurox/IronWall.sol @@ -25,7 +25,7 @@ contract IronWall is IMoveSet, BasicEffect { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256, - uint240, + uint16, uint256 ) external { // Check to see if the effect is already active @@ -68,7 +68,7 @@ contract IronWall is IMoveSet, BasicEffect { return MoveClass.Self; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/aurox/VolatilePunch.sol b/src/mons/aurox/VolatilePunch.sol index fb288cad..772eaa2e 100644 --- a/src/mons/aurox/VolatilePunch.sol +++ b/src/mons/aurox/VolatilePunch.sol @@ -46,7 +46,7 @@ contract VolatilePunch is StandardAttack { uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, - uint240, + uint16, uint256 rng ) public override { // Deal the damage to opponent diff --git a/src/mons/ekineki/BubbleBop.sol b/src/mons/ekineki/BubbleBop.sol index cb18cc3f..5aa35f92 100644 --- a/src/mons/ekineki/BubbleBop.sol +++ b/src/mons/ekineki/BubbleBop.sol @@ -39,7 +39,7 @@ contract BubbleBop is StandardAttack { uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, - uint240, + uint16, uint256 rng ) public override { uint32 effectiveCritRate = NineNineNineLib._getEffectiveCritRate(engine, battleKey, attackerPlayerIndex); diff --git a/src/mons/ekineki/NineNineNine.sol b/src/mons/ekineki/NineNineNine.sol index 0c1a6f41..614311c5 100644 --- a/src/mons/ekineki/NineNineNine.sol +++ b/src/mons/ekineki/NineNineNine.sol @@ -17,7 +17,7 @@ contract NineNineNine is IMoveSet { return "Nine Nine Nine"; } - function move(IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex, uint256, uint256, uint240, uint256) external { + function move(IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex, uint256, uint256, uint16, uint256) external { // Set crit boost for the next turn uint256 currentTurn = engine.getTurnIdForBattleState(battleKey); uint64 key = NineNineNineLib._getKey(attackerPlayerIndex); @@ -40,7 +40,7 @@ contract NineNineNine is IMoveSet { return MoveClass.Self; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/ekineki/Overflow.sol b/src/mons/ekineki/Overflow.sol index 7d3fa135..f75c87bd 100644 --- a/src/mons/ekineki/Overflow.sol +++ b/src/mons/ekineki/Overflow.sol @@ -39,7 +39,7 @@ contract Overflow is StandardAttack { uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, - uint240, + uint16, uint256 rng ) public override { uint32 effectiveCritRate = NineNineNineLib._getEffectiveCritRate(engine, battleKey, attackerPlayerIndex); diff --git a/src/mons/ekineki/SneakAttack.sol b/src/mons/ekineki/SneakAttack.sol index 3c669924..2682e158 100644 --- a/src/mons/ekineki/SneakAttack.sol +++ b/src/mons/ekineki/SneakAttack.sol @@ -35,7 +35,7 @@ contract SneakAttack is IMoveSet, BasicEffect { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256, - uint240 extraData, + uint16 extraData, uint256 rng ) external { // Check if already used this switch-in (effect present = already used) @@ -107,7 +107,7 @@ contract SneakAttack is IMoveSet, BasicEffect { return MoveClass.Special; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/embursa/HeatBeacon.sol b/src/mons/embursa/HeatBeacon.sol index b6d3f0e2..7d60ab69 100644 --- a/src/mons/embursa/HeatBeacon.sol +++ b/src/mons/embursa/HeatBeacon.sol @@ -28,7 +28,7 @@ contract HeatBeacon is IMoveSet { uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, - uint240, + uint16, uint256 ) external { // Apply burn to opposing mon @@ -56,7 +56,7 @@ contract HeatBeacon is IMoveSet { return Type.Fire; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/embursa/HoneyBribe.sol b/src/mons/embursa/HoneyBribe.sol index a9a68614..653d4d05 100644 --- a/src/mons/embursa/HoneyBribe.sol +++ b/src/mons/embursa/HoneyBribe.sol @@ -51,7 +51,7 @@ contract HoneyBribe is IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256 defenderMonIndex, - uint240, + uint16, uint256 ) external { // Heal active mon by max HP / 2**bribeLevel @@ -110,7 +110,7 @@ contract HoneyBribe is IMoveSet { return MoveClass.Self; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/embursa/Q5.sol b/src/mons/embursa/Q5.sol index f6bf895a..fb5df4e0 100644 --- a/src/mons/embursa/Q5.sol +++ b/src/mons/embursa/Q5.sol @@ -36,7 +36,7 @@ contract Q5 is IMoveSet, BasicEffect { attackerPlayerIndex = uint256(data) & type(uint128).max; } - function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256, uint240, uint256) external { + function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256, uint16, uint256) external { // Add effect to global effects engine.addEffect(2, attackerPlayerIndex, this, _packExtraData(1, attackerPlayerIndex)); @@ -58,7 +58,7 @@ contract Q5 is IMoveSet, BasicEffect { return Type.Fire; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/embursa/SetAblaze.sol b/src/mons/embursa/SetAblaze.sol index af8ae952..36cdf255 100644 --- a/src/mons/embursa/SetAblaze.sol +++ b/src/mons/embursa/SetAblaze.sol @@ -40,7 +40,7 @@ contract SetAblaze is StandardAttack { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256 defenderMonIndex, - uint240 args, + uint16 args, uint256 rng ) public override { engine.dispatchStandardAttack( diff --git a/src/mons/ghouliath/EternalGrudge.sol b/src/mons/ghouliath/EternalGrudge.sol index ad78b202..adecae58 100644 --- a/src/mons/ghouliath/EternalGrudge.sol +++ b/src/mons/ghouliath/EternalGrudge.sol @@ -30,7 +30,7 @@ contract EternalGrudge is IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256 defenderMonIndex, - uint240, + uint16, uint256 ) external { // Apply the debuff (50% debuff to both attack and special attack) @@ -69,7 +69,7 @@ contract EternalGrudge is IMoveSet { return Type.Yin; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/ghouliath/WitherAway.sol b/src/mons/ghouliath/WitherAway.sol index 8115f38c..d8094642 100644 --- a/src/mons/ghouliath/WitherAway.sol +++ b/src/mons/ghouliath/WitherAway.sol @@ -39,7 +39,7 @@ contract WitherAway is StandardAttack { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256 defenderMonIndex, - uint240 extraData, + uint16 extraData, uint256 rng ) public override { // Deal the damage and inflict panic diff --git a/src/mons/gorillax/RockPull.sol b/src/mons/gorillax/RockPull.sol index 68457ad5..f86b3094 100644 --- a/src/mons/gorillax/RockPull.sol +++ b/src/mons/gorillax/RockPull.sol @@ -40,7 +40,7 @@ contract RockPull is IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256, - uint240, + uint16, uint256 rng ) external { if (_didOtherPlayerChooseSwitch(engine, battleKey, attackerPlayerIndex)) { @@ -98,7 +98,7 @@ contract RockPull is IMoveSet { return MoveClass.Physical; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/iblivion/Brightback.sol b/src/mons/iblivion/Brightback.sol index 46f42998..e9712c1c 100644 --- a/src/mons/iblivion/Brightback.sol +++ b/src/mons/iblivion/Brightback.sol @@ -41,7 +41,7 @@ contract Brightback is IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256, - uint240, + uint16, uint256 rng ) external { (int32 damageDealt,) = AttackCalculator._calculateDamage( @@ -95,7 +95,7 @@ contract Brightback is IMoveSet { return MoveClass.Physical; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/iblivion/Loop.sol b/src/mons/iblivion/Loop.sol index 8c1b4c2e..6b418083 100644 --- a/src/mons/iblivion/Loop.sol +++ b/src/mons/iblivion/Loop.sol @@ -70,7 +70,7 @@ contract Loop is IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256, - uint240, + uint16, uint256 ) external { // Check if Loop is already active @@ -134,7 +134,7 @@ contract Loop is IMoveSet { return Type.Yang; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/iblivion/Renormalize.sol b/src/mons/iblivion/Renormalize.sol index 656dd9f6..81ac7114 100644 --- a/src/mons/iblivion/Renormalize.sol +++ b/src/mons/iblivion/Renormalize.sol @@ -42,7 +42,7 @@ contract Renormalize is IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256, - uint240, + uint16, uint256 ) external { // Set Baselight level to 3 @@ -67,7 +67,7 @@ contract Renormalize is IMoveSet { return Type.Yang; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/iblivion/UnboundedStrike.sol b/src/mons/iblivion/UnboundedStrike.sol index 6225859e..f4a3d461 100644 --- a/src/mons/iblivion/UnboundedStrike.sol +++ b/src/mons/iblivion/UnboundedStrike.sol @@ -45,7 +45,7 @@ contract UnboundedStrike is IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256, - uint240, + uint16, uint256 rng ) external { uint256 baselightLevel = BASELIGHT.getBaselightLevel(engine, battleKey, attackerPlayerIndex, attackerMonIndex); @@ -95,7 +95,7 @@ contract UnboundedStrike is IMoveSet { return MoveClass.Physical; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/inutia/ChainExpansion.sol b/src/mons/inutia/ChainExpansion.sol index a462fc3f..7d673aad 100644 --- a/src/mons/inutia/ChainExpansion.sol +++ b/src/mons/inutia/ChainExpansion.sol @@ -33,7 +33,7 @@ contract ChainExpansion is IMoveSet, BasicEffect { return keccak256(abi.encode(playerIndex, monIndex, name())); } - function move(IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex, uint256, uint256, uint240, uint256) external { + function move(IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex, uint256, uint256, uint16, uint256) external { // Check if the ability is already applied globally (EffectInstance[] memory effects, ) = engine.getEffects(battleKey, 2, 2); for (uint256 i = 0; i < effects.length; i++) { @@ -61,7 +61,7 @@ contract ChainExpansion is IMoveSet, BasicEffect { return MoveClass.Other; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/inutia/HitAndDip.sol b/src/mons/inutia/HitAndDip.sol index 36357b77..dc087ec4 100644 --- a/src/mons/inutia/HitAndDip.sol +++ b/src/mons/inutia/HitAndDip.sol @@ -39,7 +39,7 @@ contract HitAndDip is StandardAttack { uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, - uint240 extraData, + uint16 extraData, uint256 rng ) public override { // Deal the damage @@ -51,7 +51,7 @@ contract HitAndDip is StandardAttack { ); if (damage > 0) { - // extraData contains the swap index as raw uint240 + // extraData contains the swap index as raw uint16 uint256 swapIndex = uint256(extraData); engine.switchActiveMon(attackerPlayerIndex, swapIndex); } diff --git a/src/mons/inutia/Initialize.sol b/src/mons/inutia/Initialize.sol index 427147c0..d9fb1cfe 100644 --- a/src/mons/inutia/Initialize.sol +++ b/src/mons/inutia/Initialize.sol @@ -35,7 +35,7 @@ contract Initialize is IMoveSet, BasicEffect { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256, - uint240, + uint16, uint256 ) external { // Check if global KV is set @@ -83,7 +83,7 @@ contract Initialize is IMoveSet, BasicEffect { return MoveClass.Self; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/malalien/TripleThink.sol b/src/mons/malalien/TripleThink.sol index 95a24830..5b0a8d0b 100644 --- a/src/mons/malalien/TripleThink.sol +++ b/src/mons/malalien/TripleThink.sol @@ -29,7 +29,7 @@ contract TripleThink is IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256, - uint240, + uint16, uint256 ) external { // Apply the buff @@ -58,7 +58,7 @@ contract TripleThink is IMoveSet { return MoveClass.Self; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/pengym/Deadlift.sol b/src/mons/pengym/Deadlift.sol index 120974e0..065843f3 100644 --- a/src/mons/pengym/Deadlift.sol +++ b/src/mons/pengym/Deadlift.sol @@ -30,7 +30,7 @@ contract Deadlift is IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256, - uint240, + uint16, uint256 ) external { // Apply the buffs @@ -64,7 +64,7 @@ contract Deadlift is IMoveSet { return MoveClass.Self; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/pengym/DeepFreeze.sol b/src/mons/pengym/DeepFreeze.sol index 6f841a39..9dba3bd8 100644 --- a/src/mons/pengym/DeepFreeze.sol +++ b/src/mons/pengym/DeepFreeze.sol @@ -47,7 +47,7 @@ contract DeepFreeze is IMoveSet { uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, - uint240, + uint16, uint256 rng ) external { uint256 otherPlayerIndex = (attackerPlayerIndex + 1) % 2; @@ -90,7 +90,7 @@ contract DeepFreeze is IMoveSet { return MoveClass.Physical; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/pengym/PistolSquat.sol b/src/mons/pengym/PistolSquat.sol index 9bb24bee..b234624d 100644 --- a/src/mons/pengym/PistolSquat.sol +++ b/src/mons/pengym/PistolSquat.sol @@ -60,7 +60,7 @@ contract PistolSquat is StandardAttack { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256 defenderMonIndex, - uint240 extraData, + uint16 extraData, uint256 rng ) public override { // Deal the damage diff --git a/src/mons/sofabbi/Gachachacha.sol b/src/mons/sofabbi/Gachachacha.sol index a25180f8..1d49a9bb 100644 --- a/src/mons/sofabbi/Gachachacha.sol +++ b/src/mons/sofabbi/Gachachacha.sol @@ -41,7 +41,7 @@ contract Gachachacha is IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256 defenderMonIndex, - uint240, + uint16, uint256 rng ) external { uint256 chance = rng % OPP_KO_THRESHOLD_R; @@ -91,7 +91,7 @@ contract Gachachacha is IMoveSet { return MoveClass.Physical; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/sofabbi/GuestFeature.sol b/src/mons/sofabbi/GuestFeature.sol index 83fb7d6a..1b73f2f9 100644 --- a/src/mons/sofabbi/GuestFeature.sol +++ b/src/mons/sofabbi/GuestFeature.sol @@ -30,7 +30,7 @@ contract GuestFeature is IMoveSet { uint256 attackerPlayerIndex, uint256, uint256, - uint240 extraData, + uint16 extraData, uint256 rng ) external { uint256 monIndex = uint256(extraData); @@ -67,7 +67,7 @@ contract GuestFeature is IMoveSet { return MoveClass.Physical; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/sofabbi/SnackBreak.sol b/src/mons/sofabbi/SnackBreak.sol index ec40b197..7b78752b 100644 --- a/src/mons/sofabbi/SnackBreak.sol +++ b/src/mons/sofabbi/SnackBreak.sol @@ -42,7 +42,7 @@ contract SnackBreak is IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256, - uint240, + uint16, uint256 ) external { uint256 snackLevel = _getSnackLevel(engine, battleKey, attackerPlayerIndex, attackerMonIndex); @@ -78,7 +78,7 @@ contract SnackBreak is IMoveSet { return MoveClass.Self; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/volthare/DualShock.sol b/src/mons/volthare/DualShock.sol index be91b0fc..bc541f89 100644 --- a/src/mons/volthare/DualShock.sol +++ b/src/mons/volthare/DualShock.sol @@ -45,7 +45,7 @@ contract DualShock is StandardAttack { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256 defenderMonIndex, - uint240 extraData, + uint16 extraData, uint256 rng ) public override { // Deal the damage diff --git a/src/mons/volthare/MegaStarBlast.sol b/src/mons/volthare/MegaStarBlast.sol index 9ff0f9de..df7c0aa7 100644 --- a/src/mons/volthare/MegaStarBlast.sol +++ b/src/mons/volthare/MegaStarBlast.sol @@ -52,7 +52,7 @@ contract MegaStarBlast is IMoveSet { uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, - uint240, + uint16, uint256 rng ) external { // Check if Overclock is active @@ -104,7 +104,7 @@ contract MegaStarBlast is IMoveSet { return MoveClass.Special; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/volthare/RoundTrip.sol b/src/mons/volthare/RoundTrip.sol index 8880e455..1fa154fe 100644 --- a/src/mons/volthare/RoundTrip.sol +++ b/src/mons/volthare/RoundTrip.sol @@ -39,7 +39,7 @@ contract RoundTrip is StandardAttack { uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, - uint240 extraData, + uint16 extraData, uint256 rng ) public override { // Deal the damage @@ -51,7 +51,7 @@ contract RoundTrip is StandardAttack { ); if (damage > 0) { - // extraData contains the swap index as raw uint240 + // extraData contains the swap index as raw uint16 uint256 swapIndex = uint256(extraData); engine.switchActiveMon(attackerPlayerIndex, swapIndex); } diff --git a/src/mons/xmon/ContagiousSlumber.sol b/src/mons/xmon/ContagiousSlumber.sol index 5153ae73..5c349615 100644 --- a/src/mons/xmon/ContagiousSlumber.sol +++ b/src/mons/xmon/ContagiousSlumber.sol @@ -27,7 +27,7 @@ contract ContagiousSlumber is IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256 defenderMonIndex, - uint240, + uint16, uint256 ) external { // Apply sleep to self @@ -54,7 +54,7 @@ contract ContagiousSlumber is IMoveSet { return MoveClass.Other; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/xmon/NightTerrors.sol b/src/mons/xmon/NightTerrors.sol index 399b053e..fb7984a7 100644 --- a/src/mons/xmon/NightTerrors.sol +++ b/src/mons/xmon/NightTerrors.sol @@ -36,7 +36,7 @@ contract NightTerrors is IMoveSet, BasicEffect { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256, - uint240, + uint16, uint256 ) external { uint256 defenderPlayerIndex = (attackerPlayerIndex + 1) % 2; @@ -96,7 +96,7 @@ contract NightTerrors is IMoveSet, BasicEffect { return MoveClass.Special; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/xmon/Somniphobia.sol b/src/mons/xmon/Somniphobia.sol index 7916a432..628e3553 100644 --- a/src/mons/xmon/Somniphobia.sol +++ b/src/mons/xmon/Somniphobia.sol @@ -18,7 +18,7 @@ contract Somniphobia is IMoveSet, BasicEffect { return "Somniphobia"; } - function move(IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex, uint256, uint256, uint240, uint256) external { + function move(IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex, uint256, uint256, uint16, uint256) external { // Add effect globally for 6 turns (only if it's not already in global effects) (EffectInstance[] memory effects, ) = engine.getEffects(battleKey, 2, 2); for (uint256 i = 0; i < effects.length; i++) { @@ -45,7 +45,7 @@ contract Somniphobia is IMoveSet, BasicEffect { return MoveClass.Other; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/src/mons/xmon/VitalSiphon.sol b/src/mons/xmon/VitalSiphon.sol index d2f472d7..1f85f0d8 100644 --- a/src/mons/xmon/VitalSiphon.sol +++ b/src/mons/xmon/VitalSiphon.sol @@ -40,7 +40,7 @@ contract VitalSiphon is StandardAttack { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256 defenderMonIndex, - uint240 extraData, + uint16 extraData, uint256 rng ) public override { // Deal the damage diff --git a/src/moves/IMoveSet.sol b/src/moves/IMoveSet.sol index 3ca381fe..cba4b69b 100644 --- a/src/moves/IMoveSet.sol +++ b/src/moves/IMoveSet.sol @@ -13,7 +13,7 @@ interface IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256 defenderMonIndex, - uint240 extraData, + uint16 extraData, uint256 rng ) external; function priority(IEngine engine, bytes32 battleKey, uint256 attackerPlayerIndex) external view returns (uint32); @@ -22,7 +22,7 @@ interface IMoveSet { view returns (uint32); function moveType(IEngine engine, bytes32 battleKey) external view returns (Type); - function isValidTarget(IEngine engine, bytes32 battleKey, uint240 extraData) external view returns (bool); + function isValidTarget(IEngine engine, bytes32 battleKey, uint16 extraData) external view returns (bool); function moveClass(IEngine engine, bytes32 battleKey) external view returns (MoveClass); function extraDataType() external view returns (ExtraDataType); diff --git a/src/moves/StandardAttack.sol b/src/moves/StandardAttack.sol index 47bc9f51..66a464d6 100644 --- a/src/moves/StandardAttack.sol +++ b/src/moves/StandardAttack.sol @@ -51,7 +51,7 @@ contract StandardAttack is IMoveSet, Ownable { uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, - uint240, + uint16, uint256 rng ) public virtual { _move(engine, battleKey, attackerPlayerIndex, defenderMonIndex, rng); @@ -83,7 +83,7 @@ contract StandardAttack is IMoveSet, Ownable { ); } - function isValidTarget(IEngine, bytes32, uint240) public pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) public pure returns (bool) { return true; } diff --git a/test/BattleHistoryTest.sol b/test/BattleHistoryTest.sol index 005af49a..2aed9ea2 100644 --- a/test/BattleHistoryTest.sol +++ b/test/BattleHistoryTest.sol @@ -166,10 +166,10 @@ contract BattleHistoryTest is Test, BattleHelper { address p1, uint8 p0MoveIndex, uint8 p1MoveIndex, - uint240 p0ExtraData, - uint240 p1ExtraData + uint16 p0ExtraData, + uint16 p1ExtraData ) internal { - bytes32 salt = ""; + uint104 salt = 0; bytes32 p0MoveHash = keccak256(abi.encodePacked(p0MoveIndex, salt, p0ExtraData)); bytes32 p1MoveHash = keccak256(abi.encodePacked(p1MoveIndex, salt, p1ExtraData)); @@ -199,7 +199,7 @@ contract BattleHistoryTest is Test, BattleHelper { // First move - both players switch to their mon _commitRevealExecute( - battleKey, battleData.p0, battleData.p1, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + battleKey, battleData.p0, battleData.p1, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Second move - both attack (faster mon wins) @@ -216,7 +216,7 @@ contract BattleHistoryTest is Test, BattleHelper { assertEq(battleHistory.getNumBattles(BOB), 0, "Bob should have 0 battles before completion"); // First turn - both switch to their mons - _commitRevealExecute(battleKey, ALICE, BOB, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecute(battleKey, ALICE, BOB, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); // Stats should still be 0 after first turn assertEq(battleHistory.getNumBattles(ALICE), 0, "Alice should have 0 battles after switch"); diff --git a/test/BetterCPUInlineGasTest.sol b/test/BetterCPUInlineGasTest.sol index 9cccd4e5..e7b4112e 100644 --- a/test/BetterCPUInlineGasTest.sol +++ b/test/BetterCPUInlineGasTest.sol @@ -159,28 +159,28 @@ contract BetterCPUInlineGasTest is Test { // Turn 0: lead selection. Both players "switch in" a starting mon. vm.startSnapshotGas("Turn0_Lead"); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); uint256 turn0Gas = vm.stopSnapshotGas("Turn0_Lead"); engine.resetCallContext(); // Turns 1-4: both attack with move 1. Every one is flag == 2, no KOs. vm.startSnapshotGas("Turn1_BothAttack"); - cpu.selectMove(battleKey, 1, "", 0); + cpu.selectMove(battleKey, 1, uint104(0), 0); uint256 turn1Gas = vm.stopSnapshotGas("Turn1_BothAttack"); engine.resetCallContext(); vm.startSnapshotGas("Turn2_BothAttack"); - cpu.selectMove(battleKey, 1, "", 0); + cpu.selectMove(battleKey, 1, uint104(0), 0); uint256 turn2Gas = vm.stopSnapshotGas("Turn2_BothAttack"); engine.resetCallContext(); vm.startSnapshotGas("Turn3_BothAttack"); - cpu.selectMove(battleKey, 1, "", 0); + cpu.selectMove(battleKey, 1, uint104(0), 0); uint256 turn3Gas = vm.stopSnapshotGas("Turn3_BothAttack"); engine.resetCallContext(); vm.startSnapshotGas("Turn4_BothAttack"); - cpu.selectMove(battleKey, 1, "", 0); + cpu.selectMove(battleKey, 1, uint104(0), 0); uint256 turn4Gas = vm.stopSnapshotGas("Turn4_BothAttack"); engine.resetCallContext(); @@ -225,11 +225,11 @@ contract BetterCPUInlineGasTest is Test { mockCPURNG.setRNG(0); // Turn 0: lead. - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: both attack. CPU's move 1 (BP=40, attack=200, defense=10) should KO Alice. - cpu.selectMove(battleKey, 1, "", 0); + cpu.selectMove(battleKey, 1, uint104(0), 0); engine.resetCallContext(); // After the KO we should be in flag==0 (Alice forced switch). @@ -238,7 +238,7 @@ contract BetterCPUInlineGasTest is Test { // Measure the cheap-router path: Alice submits her switch via the CPU manager. vm.startSnapshotGas("Flag0_P0ForcedSwitch"); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(1)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(1)); uint256 flag0Gas = vm.stopSnapshotGas("Flag0_P0ForcedSwitch"); engine.resetCallContext(); diff --git a/test/BetterCPUTest.sol b/test/BetterCPUTest.sol index e331a72a..5965ae46 100644 --- a/test/BetterCPUTest.sol +++ b/test/BetterCPUTest.sol @@ -198,13 +198,13 @@ contract BetterCPUTest is Test { bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); // Turn 0: Both select mon 0 - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Deal some damage to Alice's mon so CPU sees it can KO // The CPU should select the high power move (index 1) to secure the KO // Set RNG to not trigger random selection mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); // Check that Alice's mon took massive damage (from high power attack) int32 aliceHpDelta = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp); @@ -243,7 +243,7 @@ contract BetterCPUTest is Test { bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); // Alice selects mon 0 (Fire type) - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // CPU should have selected Liquid mon (index 1) uint256[] memory activeIndex = engine.getActiveMonIndexForBattleState(battleKey); @@ -298,7 +298,7 @@ contract BetterCPUTest is Test { // Since neither mon resists Liquid (Alice's lead could be Liquid), CPU picks randomly // We force Fire mon to be selected mockCPURNG.setRNG(0); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Get active mons - CPU might have selected based on type calcs uint256[] memory activeIndex = engine.getActiveMonIndexForBattleState(battleKey); @@ -306,7 +306,7 @@ contract BetterCPUTest is Test { // Turn 1: CPU should detect kill threat from Fire attack and switch to Liquid if currently Fire mockCPURNG.setRNG(1); // Don't trigger random selection - cpu.selectMove(battleKey, 0, "", 0); // Alice attacks + cpu.selectMove(battleKey, 0, uint104(0), 0); // Alice attacks // If CPU started with Fire, it should switch to Liquid to survive // If CPU started with Liquid, it should stay (already resists Fire) @@ -354,12 +354,12 @@ contract BetterCPUTest is Test { bytes32 battleKey = _startBattleWithCPU(team, team); // Turn 0: Both select mon 0 - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Use the expensive attack (costs 5 stamina) // RNG = 1 won't trigger random selection (1 % 10 != 0) mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); // Stamina delta should be -5 int32 staminaDelta = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina); @@ -367,7 +367,7 @@ contract BetterCPUTest is Test { // Turn 2: Opponent rests (P4 path). New BetterCPU attacks on free turns even at low stamina. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); // Stamina should be -10 (attacked again with the 5-cost move on the free turn) staminaDelta = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina); @@ -409,12 +409,12 @@ contract BetterCPUTest is Test { bytes32 battleKey = _startBattleWithCPU(team, team); // Turn 0: Both select mon 0 - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: At full HP, CPU should prefer setup move // Set RNG to not trigger random selection mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); // Check stamina consumed (setup move costs 1) int32 staminaDelta = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina); @@ -434,28 +434,34 @@ contract BetterCPUTest is Test { moves[1] = uint256(uint160(address(mediumAttack))); moves[2] = uint256(uint160(address(strongAttack))); - Mon memory mon = _createMon(Type.Fire, 100, 50, 10); - mon.moves = moves; + // CPU mon is faster than Alice's so attack ordering is deterministic regardless + // of the engine's RNG-based speed-tie breaker. + Mon memory aliceMon = _createMonWithSpeed(Type.Fire, 100, 50, 10, 10); + aliceMon.moves = moves; + Mon memory cpuMon = _createMonWithSpeed(Type.Fire, 100, 50, 10, 100); + cpuMon.moves = moves; - // Need 2 mons per team - Mon[] memory team = new Mon[](2); - team[0] = mon; - team[1] = mon; + Mon[] memory aliceTeam = new Mon[](2); + aliceTeam[0] = aliceMon; + aliceTeam[1] = aliceMon; + Mon[] memory cpuTeam = new Mon[](2); + cpuTeam[0] = cpuMon; + cpuTeam[1] = cpuMon; - bytes32 battleKey = _startBattleWithCPU(team, team); + bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); // Turn 0: Both select mon 0 - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: CPU is at full HP, so attack first with Alice to damage CPU // Then CPU will be at non-full HP and prefer attack moves mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 2, "", 0); // Alice uses strong attack on CPU + cpu.selectMove(battleKey, 2, uint104(0), 0); // Alice uses strong attack on CPU // Now CPU's HP is damaged, next turn it should use highest damage move // Turn 2: CPU should select the strongest attack mockCPURNG.setRNG(1); // Don't trigger random - cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); // Verify significant damage was dealt (strong attack) - Alice took damage both turns int32 aliceHpDelta = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp); @@ -492,7 +498,7 @@ contract BetterCPUTest is Test { bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); // Alice selects mon 0 (Fire/Nature) - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); uint256[] memory activeIndex = engine.getActiveMonIndexForBattleState(battleKey); assertEq(activeIndex[1], 1, "CPU should select Liquid mon (resists both Fire and Nature)"); @@ -521,7 +527,7 @@ contract BetterCPUTest is Test { typeCalc.setTypeEffectiveness(Type.Fire, Type.Nature, 20); // 2x bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); uint256[] memory activeIndex = engine.getActiveMonIndexForBattleState(battleKey); assertEq(activeIndex[1], 0, "CPU should select Fire mon (2x offensive vs Nature)"); @@ -546,7 +552,7 @@ contract BetterCPUTest is Test { } bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Just verify no crash and valid index uint256[] memory activeIndex = engine.getActiveMonIndexForBattleState(battleKey); @@ -601,7 +607,7 @@ contract BetterCPUTest is Test { bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); // Turn 0: lead selection. All neutral → picks mon 0 (first). - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); uint256[] memory activeIndex = engine.getActiveMonIndexForBattleState(battleKey); assertEq(activeIndex[1], 0, "CPU should lead with mon 0"); @@ -609,12 +615,12 @@ contract BetterCPUTest is Test { // Turn 1: Alice uses Fire move (move 0). All CPU mons take equal Fire damage. // P5 materiality fails. CPU stays. Mon0 KO'd. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); // Turn 2 (forced switch): Alice signals move 1 (Liquid). CPU evaluates Liquid damage. // Mon2(Nature) resists Liquid → takes less damage → picked. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 1, "", 0); + cpu.selectMove(battleKey, 1, uint104(0), 0); engine.resetCallContext(); activeIndex = engine.getActiveMonIndexForBattleState(battleKey); assertEq(activeIndex[1], 2, "CPU should switch to Nature (resists Liquid attack)"); @@ -649,11 +655,11 @@ contract BetterCPUTest is Test { bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); // Turn 0: lead selection - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: CPU should use KO move. Alice attacks weakly. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); // Alice's mon should be KO'd int32 aliceKO = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.IsKnockedOut); @@ -680,11 +686,11 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = moves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Both attack. CPU outspeeds → KOs Alice first. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); int32 aliceKO = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.IsKnockedOut); assertEq(aliceKO, 1, "CPU should KO Alice when outspeeding"); @@ -716,11 +722,11 @@ contract BetterCPUTest is Test { typeCalc.setTypeEffectiveness(Type.Fire, Type.Liquid, 5); // 0.5x bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: CPU outsped and opponent can KO → CPU should switch to Liquid. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); uint256[] memory activeIndex = engine.getActiveMonIndexForBattleState(battleKey); assertEq(activeIndex[1], 1, "CPU should switch to Liquid when outsped in KO race"); @@ -755,11 +761,11 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = aliceMoves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: CPU should pick the cheaper KO move (cost=1). mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); // Stamina delta should be -1 (cheap move used) int32 staminaDelta = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina); @@ -792,11 +798,11 @@ contract BetterCPUTest is Test { typeCalc.setTypeEffectiveness(Type.Fire, Type.Nature, 20); // 2x bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Alice switches to mon 1 (Nature, hp=20). CPU should KO it. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, "", uint240(1)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(1)); engine.resetCallContext(); // Alice's mon 1 should be KO'd int32 aliceMon1KO = engine.getMonStateForBattle(battleKey, 0, 1, MonStateIndexName.IsKnockedOut); @@ -837,11 +843,11 @@ contract BetterCPUTest is Test { typeCalc.setTypeEffectiveness(Type.Liquid, Type.Nature, 5); // 0.5x bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Alice switches to Nature mon. CPU should use Fire attack (best damage). mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, "", uint240(1)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(1)); engine.resetCallContext(); // Alice's Nature mon should take Fire damage (500) int32 aliceHpDelta = engine.getMonStateForBattle(battleKey, 0, 1, MonStateIndexName.Hp); @@ -873,11 +879,11 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = aliceMoves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Alice switches. CPU has no affordable moves → rests. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, "", uint240(1)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(1)); engine.resetCallContext(); // CPU stamina should be unchanged (rested) int32 staminaDelta = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina); @@ -915,11 +921,11 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = aliceMoves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Alice rests. No KO possible (hp=500). CPU should use strongest move in P4. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); int32 aliceHpDelta = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp); assertEq(aliceHpDelta, -400, "CPU should use bp=80 move for 400 damage"); @@ -949,11 +955,11 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = aliceMoves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Alice rests. CPU has no affordable moves → also rests. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); int32 staminaDelta = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina); assertEq(staminaDelta, 0, "CPU should rest when no affordable moves"); @@ -994,14 +1000,14 @@ contract BetterCPUTest is Test { bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); // Lead selection: Alice is Liquid type. Liquid→Metal=1x, Liquid→Liquid=1x. Neutral. Mon0 leads. - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); uint256[] memory activeIndex = engine.getActiveMonIndexForBattleState(battleKey); assertEq(activeIndex[1], 0, "CPU should lead with Mon0 (Metal)"); // Turn 1: Alice uses Fire attack. Lethal to Metal. CPU switches to Liquid. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); activeIndex = engine.getActiveMonIndexForBattleState(battleKey); assertEq(activeIndex[1], 1, "CPU should switch to Liquid to survive lethal Fire attack"); @@ -1027,11 +1033,11 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = moves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Alice attacks weakly. CPU stays and attacks back. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); uint256[] memory activeIndex = engine.getActiveMonIndexForBattleState(battleKey); assertEq(activeIndex[1], 0, "CPU should stay when damage is low"); @@ -1056,10 +1062,11 @@ contract BetterCPUTest is Test { uint256[] memory moves = new uint256[](1); moves[0] = uint256(uint160(address(strongAttack))); + // CPU mons are faster so they attack first and get to deal damage before being KO'd. Mon[] memory cpuTeam = new Mon[](2); - cpuTeam[0] = _createMon(Type.Fire, 100, 50, 10); + cpuTeam[0] = _createMonWithSpeed(Type.Fire, 100, 50, 10, 100); cpuTeam[0].moves = moves; - cpuTeam[1] = _createMon(Type.Fire, 100, 50, 10); + cpuTeam[1] = _createMonWithSpeed(Type.Fire, 100, 50, 10, 100); cpuTeam[1].moves = moves; Mon[] memory aliceTeam = new Mon[](2); @@ -1069,11 +1076,11 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = moves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Both lethal, no material improvement → CPU stays and attacks. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); // CPU should have stayed (attacked, not switched) int32 aliceHpDelta = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp); @@ -1111,14 +1118,14 @@ contract BetterCPUTest is Test { typeCalc.setTypeEffectiveness(Type.Fire, Type.Liquid, 5); // 0.5x bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); uint256[] memory activeIndex = engine.getActiveMonIndexForBattleState(battleKey); assertEq(activeIndex[1], 0, "CPU should lead with Mon0"); // Turn 1: Alice Fire attack → lethal to Metal, Liquid survives → switch. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); activeIndex = engine.getActiveMonIndexForBattleState(battleKey); assertEq(activeIndex[1], 1, "CPU should switch to Liquid (materially better)"); @@ -1148,11 +1155,11 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = aliceMoves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Alice uses Self move. CPU skips P5, attacks in P6. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); uint256[] memory activeIndex = engine.getActiveMonIndexForBattleState(battleKey); assertEq(activeIndex[1], 0, "CPU should stay when opponent uses Self move"); @@ -1196,11 +1203,11 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = aliceMoves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Alice attacks weakly. CPU uses best move in P6. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); int32 aliceHpDelta = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp); assertEq(aliceHpDelta, -400, "CPU should use bp=80 for 400 damage"); @@ -1237,11 +1244,11 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = aliceMoves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: CPU should pick cheaper move (cost=1). mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); int32 staminaDelta = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina); assertEq(staminaDelta, -1, "CPU should pick cheaper move within damage threshold"); @@ -1278,11 +1285,11 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = aliceMoves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: CPU should pick bp=100 (cost=3) since bp=50 is outside threshold. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); int32 staminaDelta = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina); assertEq(staminaDelta, -3, "CPU should pick strongest move when cheap one is outside threshold"); @@ -1314,11 +1321,11 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = aliceMoves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: CPU can't afford moves → rests. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); int32 staminaDelta = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina); assertEq(staminaDelta, 0, "CPU should rest when no affordable moves"); @@ -1349,11 +1356,11 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = aliceMoves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: CPU exhausted, switches to Mon1. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); uint256[] memory activeIndex = engine.getActiveMonIndexForBattleState(battleKey); assertEq(activeIndex[1], 1, "CPU should switch when exhausted and switch available"); @@ -1397,12 +1404,12 @@ contract BetterCPUTest is Test { // Set preferred move: monIndex=0, key=CONFIG_PREFERRED_MOVE(0), value=2 (moveIndex 1 + 1) cpu.setMonConfig(0, 0, 2); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: CPU should use preferred move (bp=90). Damage = 90*50/10 = 450. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); int32 aliceHpDelta = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp); assertEq(aliceHpDelta, -450, "CPU should use preferred move (bp=90) within threshold"); @@ -1444,12 +1451,12 @@ contract BetterCPUTest is Test { // Set preferred move: monIndex=0, key=CONFIG_PREFERRED_MOVE(0), value=2 (moveIndex 1 + 1) cpu.setMonConfig(0, 0, 2); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Preferred too weak → CPU uses bp=100. Damage = 100*50/10 = 500. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); int32 aliceHpDelta = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp); assertEq(aliceHpDelta, -500, "CPU should ignore preferred move when too weak"); @@ -1491,19 +1498,19 @@ contract BetterCPUTest is Test { // Set switch-in move: monIndex=0, key=CONFIG_SWITCH_IN_MOVE(1), value=2 (moveIndex 1 + 1) cpu.setMonConfig(0, 1, 2); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Alice rests (P4 safe turn). CPU uses switch-in move (Self, bp=0). mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); int32 aliceHpDeltaTurn1 = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp); assertEq(aliceHpDeltaTurn1, 0, "Turn 1: CPU should use Self switch-in move (no damage)"); // Turn 2: Alice rests again. Switch-in move already used → normal P4 (best damage). mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); int32 aliceHpDeltaTurn2 = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp); assertEq(aliceHpDeltaTurn2, -250, "Turn 2: CPU should use attack move (damage 250)"); @@ -1545,19 +1552,19 @@ contract BetterCPUTest is Test { // Set switch-in move for Mon0 cpu.setMonConfig(0, 1, 2); // moveIndex 1 + 1 - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Alice rests. CPU uses switch-in Self move. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); int32 aliceHpDelta = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp); assertEq(aliceHpDelta, 0, "Turn 1: switch-in Self move fires (no damage)"); // Turn 2: Alice rests. CPU attacks normally (switch-in already used). mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); // Turn 3: Alice switches to mon 1. CPU re-evaluates. // On the switch turn, the CPU gets the switch-in move bit cleared for Mon0 when switching. @@ -1597,11 +1604,11 @@ contract BetterCPUTest is Test { typeCalc.setTypeEffectiveness(Type.Fire, Type.Liquid, 5); // 0.5x so Liquid is a viable switch bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: Both can KO. Speed tie → _weGoFirst returns false → CPU should switch. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); uint256[] memory activeIndex = engine.getActiveMonIndexForBattleState(battleKey); assertEq(activeIndex[1], 1, "CPU should switch on speed tie (play it safe)"); @@ -1631,11 +1638,11 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = aliceMoves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: CPU priority 5 > Alice priority 1 → CPU goes first, KOs Alice. mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); int32 aliceKO = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.IsKnockedOut); assertEq(aliceKO, 1, "CPU should KO Alice with higher priority move"); @@ -1670,7 +1677,7 @@ contract BetterCPUTest is Test { aliceTeam[1].moves = aliceMoves; bytes32 battleKey = _startBattleWithCPU(aliceTeam, cpuTeam); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1: CPU can't afford moves. Has switch option but P5 won't trigger (10 damage = 5%). // Falls through to P6: no moves. Switch available → might switch. @@ -1680,7 +1687,7 @@ contract BetterCPUTest is Test { // To force no-op, make team size 1? Can't, validator requires >= 2 for MONS_PER_TEAM. // Alternative: test that stamina is unchanged (CPU didn't attack). mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); int32 staminaDelta = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina); assertEq(staminaDelta, 0, "CPU stamina should be unchanged (couldn't afford attack)"); diff --git a/test/CPUTest.sol b/test/CPUTest.sol index 6cfaffd3..3b20acba 100644 --- a/test/CPUTest.sol +++ b/test/CPUTest.sol @@ -217,7 +217,7 @@ contract CPUTest is Test { // Alice selects mon 2, CPU selects mon 1 mockCPURNG.setRNG(1); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(2)); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(2)); engine.resetCallContext(); // Assert active mon index for both p0 and p1 are correct assertEq(engine.getActiveMonIndexForBattleState(battleKey)[0], 2); @@ -232,7 +232,7 @@ contract CPUTest is Test { // Alice KO's the CPU's mon, the CPU chooses no op mockCPURNG.setRNG(0); // [no op, move 1, move 2, swap 0, swap 2, swap 3] and we want no op at index 0 - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); engine.resetCallContext(); // Check that the CPU now has 3 moves, all of which are switching to mon index 0, 2, or 3 { @@ -250,7 +250,7 @@ contract CPUTest is Test { } // Alice chooses no op (choice is irrelevant here), CPU chooses to switch to mon index 0 - cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); // Assert the CPU now has mon index 0 as the active mon assertEq(engine.getActiveMonIndexForBattleState(battleKey)[1], 0); @@ -266,7 +266,7 @@ contract CPUTest is Test { mockCPURNG.setRNG(2); // [no op, move 1, move 2, swap 2, swap 3] and we want move 2 at index 2 // (note that the swaps are 0-indexed, and the moves are 1-indexed to refer to the above variable // naming convention, sorry D: ) - cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); // Assert that there are now 3 moves, switching to mon index 2, 3, and no op (all stamina has been consumed) { @@ -277,7 +277,7 @@ contract CPUTest is Test { // Alice chooses no op, CPU chooses swapping to mon index 3 mockCPURNG.setRNG(2); // [no op, swap 2, swap 3] and we want swap 3 at index 2 - cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + cpu.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); // Assert the CPU now has mon index 3 as the active mon assertEq(engine.getActiveMonIndexForBattleState(battleKey)[1], 3); @@ -370,15 +370,15 @@ contract CPUTest is Test { // Verify that calculateMove returns the correct move CPUContext memory cpuCtx = engine.getCPUContext(battleKey); - (uint128 moveIndex, uint240 extraData) = playerCPU.calculateMove(cpuCtx, 0, 0); + (uint128 moveIndex, uint16 extraData) = playerCPU.calculateMove(cpuCtx, 0, 0); assertEq(moveIndex, 0); assertEq(extraData, 0); // Execute the turn - playerCPU.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(1)); + playerCPU.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(1)); engine.resetCallContext(); // Second turn: p0 sets move 1 for PlayerCPU (should override previous move) - playerCPU.setMove(battleKey, 1, uint240(42)); + playerCPU.setMove(battleKey, 1, uint16(42)); // Verify that calculateMove now returns the new move cpuCtx = engine.getCPUContext(battleKey); @@ -387,7 +387,7 @@ contract CPUTest is Test { assertEq(uint256(extraData), 42); // Execute another turn to verify the flow continues to work - playerCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + playerCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); } @@ -455,7 +455,7 @@ contract CPUTest is Test { bytes32 battleKey = okayCPU.startBattle(proposal); // Player switches in mon index 0 (Fire type) - okayCPU.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + okayCPU.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Get active index for battle, it should be the resisted mon uint256[] memory activeIndex = engine.getActiveMonIndexForBattleState(battleKey); @@ -506,11 +506,11 @@ contract CPUTest is Test { bytes32 battleKey = okayCPU.startBattle(proposal); // Turn 0, both player send in mon index 0 - okayCPU.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + okayCPU.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1, player rests, CPU should select no op because the move costs too much stamina mockCPURNG.setRNG(1); - okayCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + okayCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); } @@ -559,17 +559,17 @@ contract CPUTest is Test { bytes32 battleKey = okayCPU.startBattle(proposal); // Turn 0, both player send in mon index 0 - okayCPU.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + okayCPU.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1, player rests, CPU should select move index 0 mockCPURNG.setRNG(1); // This triggers the OkayCPU to select a move, which should set its stamina delta to be -3 - okayCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + okayCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); // Assert the stamina delta for P1's active mon is -3 assertEq(engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina), -3); // Turn 2, player rests, CPU should rest as well - okayCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + okayCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); // Assert the stamina delta for P1's active mon is still -3 (it didn't go down more) assertEq(engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina), -3); @@ -621,10 +621,10 @@ contract CPUTest is Test { bytes32 battleKey = okayCPU.startBattle(proposal); // Turn 0, both player send in mon index 0 - okayCPU.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + okayCPU.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1, p0 rests, CPU should select move index 1 (self move) - okayCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + okayCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); // Assert that the stamina delta is -1 for p1's active mon int32 staminaDelta = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina); @@ -678,10 +678,10 @@ contract CPUTest is Test { bytes32 battleKey = okayCPU.startBattle(proposal); // Turn 0, both player send in mon index 0 - okayCPU.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + okayCPU.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1, p0 rests, CPU should select move index 1 (self move) - okayCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + okayCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); // Assert that the stamina delta is -1 for p1's active mon int32 staminaDelta = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina); @@ -735,14 +735,14 @@ contract CPUTest is Test { bytes32 battleKey = okayCPU.startBattle(proposal); // Turn 0, both player send in mon index 0 - okayCPU.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0)); + okayCPU.selectMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0)); engine.resetCallContext(); // Turn 1, set RNG to trigger smart random select and pick move index 1 // RNG needs: (RNG % 6 == 5) to trigger smart random, (RNG % 3 != 0) to not switch, ((RNG >> 8) % 2 == 1) to pick move 1 // 257 satisfies all: 257 % 6 = 5, 257 % 3 = 2, (257 >> 8) = 1 // So both mons should take 1 damage, as p0 also selects the damage move mockCPURNG.setRNG(257); - okayCPU.selectMove(battleKey, 1, "", 0); + okayCPU.selectMove(battleKey, 1, uint104(0), 0); engine.resetCallContext(); // Assert that the hp delta is -1 for p0's active mon and p1's active mon int32 hpDelta = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp); @@ -753,7 +753,7 @@ contract CPUTest is Test { // Turn 2, set RNG to be 0 (do not trigger short circuit) // CPU should select no-op because no type advantage is currently set mockCPURNG.setRNG(0); - okayCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + okayCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); // Assert that the hp delta is still -1 for p0's active mon hpDelta = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp); @@ -763,7 +763,7 @@ contract CPUTest is Test { typeCalc.setTypeEffectiveness(Type.Liquid, Type.Liquid, 2); // Now the CPU should select the damage move (move index 1) because it has a type advantage - okayCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, "", 0); + okayCPU.selectMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0); engine.resetCallContext(); // Assert that the hp delta is -2 for p0's active mon hpDelta = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Hp); diff --git a/test/DefaultCommitManagerTest.sol b/test/DefaultCommitManagerTest.sol index 8ba783fe..b3da8de8 100644 --- a/test/DefaultCommitManagerTest.sol +++ b/test/DefaultCommitManagerTest.sol @@ -85,19 +85,19 @@ contract DefaultCommitManagerTest is Test, BattleHelper { // Alice commits vm.startPrank(ALICE); uint8 moveIndex = SWITCH_MOVE_INDEX; - bytes32 moveHash = keccak256(abi.encodePacked(moveIndex, bytes32(""), uint240(0))); + bytes32 moveHash = keccak256(abi.encodePacked(moveIndex, uint104(0), uint16(0))); commitManager.commitMove(battleKey, moveHash); // Alice tries to reveal vm.expectRevert(DefaultCommitManager.NotYetRevealed.selector); - commitManager.revealMove(battleKey, moveIndex, bytes32(""), uint240(0), false); + commitManager.revealMove(battleKey, moveIndex, uint104(0), uint16(0), false); } function test_RevealBeforeSelfCommit() public { bytes32 battleKey = _startBattle(validator, engine, defaultOracle, defaultRegistry, matchmaker, address(commitManager)); // Alice sets commitment _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Bob sets commitment _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, NO_OP_MOVE_INDEX, 0, 0); @@ -108,13 +108,13 @@ contract DefaultCommitManagerTest is Test, BattleHelper { // Alice's turn again to move vm.startPrank(ALICE); vm.expectRevert(DefaultCommitManager.RevealBeforeSelfCommit.selector); - commitManager.revealMove(battleKey, NO_OP_MOVE_INDEX, bytes32(""), 0, false); + commitManager.revealMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0, false); } function test_BattleNotYetStarted() public { vm.startPrank(ALICE); vm.expectRevert(DefaultCommitManager.BattleNotYetStarted.selector); - commitManager.revealMove(bytes32(0), NO_OP_MOVE_INDEX, bytes32(""), 0, false); + commitManager.revealMove(bytes32(0), NO_OP_MOVE_INDEX, uint104(0), 0, false); vm.startPrank(BOB); vm.expectRevert(DefaultCommitManager.BattleNotYetStarted.selector); commitManager.commitMove(bytes32(0), bytes32(0)); @@ -127,7 +127,7 @@ contract DefaultCommitManagerTest is Test, BattleHelper { engine.end(battleKey); vm.startPrank(ALICE); vm.expectRevert(DefaultCommitManager.BattleAlreadyComplete.selector); - commitManager.revealMove(battleKey, NO_OP_MOVE_INDEX, bytes32(""), 0, false); + commitManager.revealMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0, false); vm.startPrank(BOB); vm.expectRevert(DefaultCommitManager.BattleAlreadyComplete.selector); commitManager.commitMove(battleKey, bytes32(0)); @@ -157,7 +157,7 @@ contract DefaultCommitManagerTest is Test, BattleHelper { vm.startPrank(ALICE); commitManager.commitMove(battleKey, bytes32("1")); vm.startPrank(BOB); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, bytes32(""), uint240(0), false); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(0), false); vm.warp(TIMEOUT * validator.PREV_TURN_MULTIPLIER() + 1); engine.end(battleKey); assertEq(engine.getWinner(battleKey), BOB); diff --git a/test/EngineGasTest.sol b/test/EngineGasTest.sol index c80bd59a..1dae1d6e 100644 --- a/test/EngineGasTest.sol +++ b/test/EngineGasTest.sol @@ -49,11 +49,6 @@ contract EngineGasTest is Test, BattleHelper { TestTeamRegistry defaultRegistry; DefaultMatchmaker matchmaker; - // Helper to pack StatBoostsMove extraData: lower 60 bits = playerIndex, next 60 bits = monIndex, next 60 bits = statIndex, upper 60 bits = boostAmount - function _packStatBoost(uint256 playerIndex, uint256 monIndex, uint256 statIndex, int32 boostAmount) internal pure returns (uint240) { - return uint240(playerIndex | (monIndex << 60) | (statIndex << 120) | (uint256(uint32(boostAmount)) << 180)); - } - function setUp() public { defaultOracle = new DefaultRandomnessOracle(); engine = new Engine(0, 0, 0); @@ -142,16 +137,16 @@ contract EngineGasTest is Test, BattleHelper { // - Alice swaps in mon index 3 // - Alice rests, Bob KOs vm.startSnapshotGas("FirstBattle"); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); // Alice uses burn, Bob uses frostbite _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, 1, 0, 0); // Bob is mon index 0, we boost attack by 90% - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 2, uint240(1), _packStatBoost(1, 0, uint256(MonStateIndexName.Attack), int32(90))); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 2, uint16(1), _packStatBoost(1, 0, uint256(MonStateIndexName.Attack), int32(90))); // Alice is now mon index 1, Bob is mon index 0 _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 2, 3, _packStatBoost(0, 1, uint256(MonStateIndexName.Attack), int32(90)), 0); // Alice swaps in mon index 0 vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0), true); engine.resetCallContext(); // Alice is now mon index 0, Bob rests _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 2, NO_OP_MOVE_INDEX, _packStatBoost(0, 0, uint256(MonStateIndexName.Attack), int32(90)), 0); @@ -159,7 +154,7 @@ contract EngineGasTest is Test, BattleHelper { _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 3, NO_OP_MOVE_INDEX, 0, 0); // Bob sends in mon index 1 vm.startPrank(BOB); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), true); engine.resetCallContext(); // Alice rests, Bob uses self-stat boost _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 2, 0, _packStatBoost(1, 1, uint256(MonStateIndexName.Attack), int32(90))); @@ -167,13 +162,13 @@ contract EngineGasTest is Test, BattleHelper { _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 3, 0, 0); // Alice swaps in mon index 2 vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(2), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(2), true); engine.resetCallContext(); // Alice rests, Bob KOs _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 3, 0, 0); // Alice swaps in mon index 3 vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(3), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(3), true); engine.resetCallContext(); // Alice rests, Bob KOs _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 3, 0, 0); @@ -223,24 +218,24 @@ contract EngineGasTest is Test, BattleHelper { // - Both players send in mon 0 vm.startSnapshotGas("SecondBattle"); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); // - Alice sets up self-stat boost (move 3), Bob sets up Burn (move 1) _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 3, 1, _packStatBoost(0, 0, uint256(MonStateIndexName.Attack), int32(90)), 0); // - Alice KOs Bob (move 0 = damage) _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 0, NO_OP_MOVE_INDEX, 0, 0); // - Bob swaps in mon index 1 vm.startPrank(BOB); - commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint240(1), true); + commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint16(1), true); engine.resetCallContext(); // - Alice swaps in mon index 1, Bob sets up Frostbite (move 2) - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, 2, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, 2, uint16(1), 0); // - Alice sets up self-stat boost (move 3, playerIndex=0, monIndex=1), Bob rests _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 3, NO_OP_MOVE_INDEX, _packStatBoost(0, 1, uint256(MonStateIndexName.Attack), int32(90)), 0); // - Alice KOs Bob (move 0) _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 0, NO_OP_MOVE_INDEX, 0, 0); // - Bob sends in mon index 2 vm.startPrank(BOB); - commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint240(2), true); + commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint16(2), true); engine.resetCallContext(); // - Alice rests, Bob uses self-stat boost (move 3, playerIndex=1, monIndex=2) _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, NO_OP_MOVE_INDEX, 3, 0, _packStatBoost(1, 2, uint256(MonStateIndexName.Attack), int32(90))); @@ -248,7 +243,7 @@ contract EngineGasTest is Test, BattleHelper { _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, NO_OP_MOVE_INDEX, 0, 0, 0); // - Alice swaps in mon index 2 vm.startPrank(ALICE); - commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint240(2), true); + commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint16(2), true); engine.resetCallContext(); // - Alice uses self-stat boost (move 3, p0 mon2), Bob uses self-stat boost (move 3, p1 mon2) _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 3, 3, _packStatBoost(0, 2, uint256(MonStateIndexName.Attack), int32(90)), _packStatBoost(1, 2, uint256(MonStateIndexName.Attack), int32(90))); @@ -256,7 +251,7 @@ contract EngineGasTest is Test, BattleHelper { _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 0, NO_OP_MOVE_INDEX, 0, 0); // - Bob sends in mon index 3 vm.startPrank(BOB); - commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint240(3), true); + commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint16(3), true); engine.resetCallContext(); // - Alice KOs Bob (move 0) _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 0, NO_OP_MOVE_INDEX, 0, 0); @@ -283,16 +278,16 @@ contract EngineGasTest is Test, BattleHelper { // Battle 3: Exact same sequence as Battle 1 vm.startSnapshotGas("ThirdBattle"); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); // Alice uses burn, Bob uses frostbite _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, 0, 1, 0, 0); // Bob is mon index 0, we boost attack by 90% - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, SWITCH_MOVE_INDEX, 2, uint240(1), _packStatBoost(1, 0, uint256(MonStateIndexName.Attack), int32(90))); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, SWITCH_MOVE_INDEX, 2, uint16(1), _packStatBoost(1, 0, uint256(MonStateIndexName.Attack), int32(90))); // Alice is now mon index 1, Bob is mon index 0 _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, 2, 3, _packStatBoost(0, 1, uint256(MonStateIndexName.Attack), int32(90)), 0); // Alice swaps in mon index 0 vm.startPrank(ALICE); - commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint240(0), true); + commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint16(0), true); engine.resetCallContext(); // Alice is now mon index 0, Bob rests _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, 2, NO_OP_MOVE_INDEX, _packStatBoost(0, 0, uint256(MonStateIndexName.Attack), int32(90)), 0); @@ -300,7 +295,7 @@ contract EngineGasTest is Test, BattleHelper { _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, 3, NO_OP_MOVE_INDEX, 0, 0); // Bob sends in mon index 1 vm.startPrank(BOB); - commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint240(1), true); + commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint16(1), true); engine.resetCallContext(); // Alice rests, Bob uses self-stat boost _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, NO_OP_MOVE_INDEX, 2, 0, _packStatBoost(1, 1, uint256(MonStateIndexName.Attack), int32(90))); @@ -308,13 +303,13 @@ contract EngineGasTest is Test, BattleHelper { _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, NO_OP_MOVE_INDEX, 3, 0, 0); // Alice swaps in mon index 2 vm.startPrank(ALICE); - commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint240(2), true); + commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint16(2), true); engine.resetCallContext(); // Alice rests, Bob KOs _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, NO_OP_MOVE_INDEX, 3, 0, 0); // Alice swaps in mon index 3 vm.startPrank(ALICE); - commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint240(3), true); + commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint16(3), true); engine.resetCallContext(); // Alice rests, Bob KOs _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, NO_OP_MOVE_INDEX, 3, 0, 0); @@ -390,7 +385,7 @@ contract EngineGasTest is Test, BattleHelper { vm.warp(vm.getBlockTimestamp() + 1); vm.startSnapshotGas("Battle1_Execute"); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey1, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); // Both switch in mon 0 + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey1, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); // Both switch in mon 0 _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey1, 0, 0, 0, 0); // Both attack - one dies // After this, battle should end uint256 execute1 = vm.stopSnapshotGas("Battle1_Execute"); @@ -404,7 +399,7 @@ contract EngineGasTest is Test, BattleHelper { vm.warp(vm.getBlockTimestamp() + 1); vm.startSnapshotGas("Battle2_Execute"); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); // Both switch in mon 0 + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); // Both switch in mon 0 _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 0, 0, 0, 0); // Both attack - one dies uint256 execute2 = vm.stopSnapshotGas("Battle2_Execute"); @@ -469,7 +464,7 @@ contract EngineGasTest is Test, BattleHelper { vm.warp(vm.getBlockTimestamp() + 1); vm.startSnapshotGas("B1_Execute"); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey1, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey1, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); // Check after switch (BattleConfigView memory cfgAfterSwitch,) = engine.getBattle(battleKey1); @@ -510,7 +505,7 @@ contract EngineGasTest is Test, BattleHelper { console.log("After B2 setup - packedP1EffectsCount:", cfg2.packedP1EffectsCount); vm.startSnapshotGas("B2_Execute"); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); // Both apply effect to each other (adds 2 effects - should REUSE slots) _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 0, 0, 0, 0); // Both attack - KO @@ -582,7 +577,7 @@ contract EngineGasTest is Test, BattleHelper { vm.warp(vm.getBlockTimestamp() + 1); vm.startSnapshotGas("External_Execute"); - _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, externalBattleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, externalBattleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, externalBattleKey, 0, 0, 0, 0); uint256 externalExecute = vm.stopSnapshotGas("External_Execute"); @@ -603,7 +598,7 @@ contract EngineGasTest is Test, BattleHelper { vm.warp(vm.getBlockTimestamp() + 1); vm.startSnapshotGas("Inline_Execute"); - _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, inlineBattleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, inlineBattleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, inlineBattleKey, 0, 0, 0, 0); uint256 inlineExecute = vm.stopSnapshotGas("Inline_Execute"); @@ -664,7 +659,7 @@ contract EngineGasTest is Test, BattleHelper { new IEngineHook[](0), simpleRuleset, address(commitManager) ); vm.warp(vm.getBlockTimestamp() + 1); - _commitRevealExecuteForEngine(engine, commitManager, battleKey1, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForEngine(engine, commitManager, battleKey1, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); _commitRevealExecuteForEngine(engine, commitManager, battleKey1, 0, 0, 0, 0); // Get final HP deltas @@ -684,7 +679,7 @@ contract EngineGasTest is Test, BattleHelper { new IEngineHook[](0), inlineRuleset, address(inlineCM) ); vm.warp(vm.getBlockTimestamp() + 1); - _commitRevealExecuteForEngine(inlineEngine, inlineCM, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForEngine(inlineEngine, inlineCM, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); _commitRevealExecuteForEngine(inlineEngine, inlineCM, battleKey2, 0, 0, 0, 0); // Get final HP deltas @@ -756,10 +751,10 @@ contract EngineGasTest is Test, BattleHelper { bytes32 battleKey, uint8 aliceMoveIndex, uint8 bobMoveIndex, - uint240 aliceExtraData, - uint240 bobExtraData + uint16 aliceExtraData, + uint16 bobExtraData ) internal { - bytes32 salt = ""; + uint104 salt = 0; bytes32 aliceMoveHash = keccak256(abi.encodePacked(aliceMoveIndex, salt, aliceExtraData)); bytes32 bobMoveHash = keccak256(abi.encodePacked(bobMoveIndex, salt, bobExtraData)); uint256 turnId = eng.getTurnIdForBattleState(battleKey); diff --git a/test/EngineGlobalKVTest.sol b/test/EngineGlobalKVTest.sol index f3d42257..7b1f7990 100644 --- a/test/EngineGlobalKVTest.sol +++ b/test/EngineGlobalKVTest.sol @@ -30,12 +30,12 @@ contract EngineGlobalKVTest is Test, BattleHelper { DefaultValidator validator; // Arbitrary keys used throughout the tests. - uint240 constant KEY_A = 1001; - uint240 constant KEY_B = 1002; - uint240 constant KEY_C = 1003; - uint240 constant KEY_D = 1004; - uint240 constant KEY_E = 1005; - uint240 constant KEY_F = 1006; + uint16 constant KEY_A = 1001; + uint16 constant KEY_B = 1002; + uint16 constant KEY_C = 1003; + uint16 constant KEY_D = 1004; + uint16 constant KEY_E = 1005; + uint16 constant KEY_F = 1006; function setUp() public { mockOracle = new MockRandomnessOracle(); @@ -71,7 +71,7 @@ contract EngineGlobalKVTest is Test, BattleHelper { defaultRegistry.setTeam(BOB, team); battleKey = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); } @@ -82,22 +82,22 @@ contract EngineGlobalKVTest is Test, BattleHelper { defaultRegistry.setTeam(BOB, team); battleKey2 = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); } /// @dev Both players use the mock write-move with their respective keys. - function _bothWrite(bytes32 battleKey, uint240 aliceKey, uint240 bobKey) internal { + function _bothWrite(bytes32 battleKey, uint16 aliceKey, uint16 bobKey) internal { _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, 0, aliceKey, bobKey); } /// @dev Alice writes; Bob rests. - function _aliceWrites(bytes32 battleKey, uint240 aliceKey) internal { + function _aliceWrites(bytes32 battleKey, uint16 aliceKey) internal { _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, aliceKey, 0); } /// @dev Bob writes; Alice rests. - function _bobWrites(bytes32 battleKey, uint240 bobKey) internal { + function _bobWrites(bytes32 battleKey, uint16 bobKey) internal { _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 0, 0, bobKey); } @@ -127,7 +127,8 @@ contract EngineGlobalKVTest is Test, BattleHelper { bytes32 firstPacked = view1.globalKVEntries[0].value; // Second call re-writes via encoded value bump so we can confirm it changed. - uint240 extraData = KEY_A | (uint240(42) << 64); + // Layout: bits 0..9 = key, bits 10..15 = value. + uint16 extraData = KEY_A | (uint16(42) << 10); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, extraData, 0); (BattleConfigView memory view2,) = engine.getBattle(battleKey); diff --git a/test/EngineOptimizationTest.sol b/test/EngineOptimizationTest.sol index 943bdb22..607f235a 100644 --- a/test/EngineOptimizationTest.sol +++ b/test/EngineOptimizationTest.sol @@ -116,7 +116,7 @@ contract EngineOptimizationTest is Test, BattleHelper { // Switch in mons _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses effect move on Bob (Bob does NoOp) @@ -175,7 +175,7 @@ contract EngineOptimizationTest is Test, BattleHelper { ); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, 0, 0); @@ -248,7 +248,7 @@ contract EngineOptimizationTest is Test, BattleHelper { // Switch in mons _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both use 5-stamina move. After: -5 from move + 1 from RoundEnd regen = -4 @@ -318,7 +318,7 @@ contract EngineOptimizationTest is Test, BattleHelper { ); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both use 5-stamina move: staminaDelta = -5 + 1 (RoundEnd) = -4 @@ -393,7 +393,7 @@ contract EngineOptimizationTest is Test, BattleHelper { ); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Use 1-stamina move: -1 from move + 1 from RoundEnd regen = 0 (not +1) @@ -414,7 +414,7 @@ contract EngineOptimizationTest is Test, BattleHelper { _forceP1Switch(testEngine, signedManager, battleKey); - _executeSinglePlayerMoveAndReset(testEngine, signedManager, battleKey, BOB, uint240(1)); + _executeSinglePlayerMoveAndReset(testEngine, signedManager, battleKey, BOB, uint16(1)); uint256[] memory activeMons = testEngine.getActiveMonIndexForBattleState(battleKey); assertEq(activeMons[1], 1, "P1 should switch to mon 1"); @@ -433,7 +433,7 @@ contract EngineOptimizationTest is Test, BattleHelper { vm.startPrank(ALICE); vm.expectRevert(DefaultCommitManager.PlayerNotAllowed.selector); - signedManager.executeSinglePlayerMove(battleKey, SWITCH_MOVE_INDEX, bytes32(0), uint240(1)); + signedManager.executeSinglePlayerMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(1)); vm.stopPrank(); } @@ -442,12 +442,12 @@ contract EngineOptimizationTest is Test, BattleHelper { _startSignedInlineSwitchBattle(false); _commitRevealExecuteForAliceAndBob( - testEngine, signedManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + testEngine, signedManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); vm.startPrank(BOB); vm.expectRevert(SignedCommitManager.NotSinglePlayerTurn.selector); - signedManager.executeSinglePlayerMove(battleKey, SWITCH_MOVE_INDEX, bytes32(0), uint240(1)); + signedManager.executeSinglePlayerMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(1)); vm.stopPrank(); } @@ -456,11 +456,11 @@ contract EngineOptimizationTest is Test, BattleHelper { _startSignedInlineSwitchBattle(false); _forceP1Switch(testEngine, signedManager, battleKey); - _executeSinglePlayerMoveAndReset(testEngine, signedManager, battleKey, BOB, uint240(1)); + _executeSinglePlayerMoveAndReset(testEngine, signedManager, battleKey, BOB, uint16(1)); uint256 turnBefore = testEngine.getTurnIdForBattleState(battleKey); _commitRevealExecuteForAliceAndBob( - testEngine, signedManager, battleKey, NO_OP_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + testEngine, signedManager, battleKey, NO_OP_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); assertEq(testEngine.getTurnIdForBattleState(battleKey), turnBefore + 1, "Normal fallback should execute"); @@ -473,7 +473,7 @@ contract EngineOptimizationTest is Test, BattleHelper { _forceP1Switch(testEngine, signedManager, battleKey); vm.prank(BOB); - signedManager.revealMove(battleKey, SWITCH_MOVE_INDEX, bytes32(0), uint240(1), true); + signedManager.revealMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(1), true); testEngine.resetCallContext(); uint256[] memory activeMons = testEngine.getActiveMonIndexForBattleState(battleKey); @@ -489,11 +489,11 @@ contract EngineOptimizationTest is Test, BattleHelper { (Engine testEngine, SignedCommitManager signedManager, bytes32 battleKey) = _startSignedInlineSwitchBattle(true); _forceP1Switch(testEngine, signedManager, battleKey); - _executeSinglePlayerMoveAndReset(testEngine, signedManager, battleKey, BOB, uint240(1)); + _executeSinglePlayerMoveAndReset(testEngine, signedManager, battleKey, BOB, uint16(1)); assertEq(testEngine.getPlayerSwitchForTurnFlagForBattleState(battleKey), 0, "P0 should be forced to switch"); - _executeSinglePlayerMoveAndReset(testEngine, signedManager, battleKey, ALICE, uint240(1)); + _executeSinglePlayerMoveAndReset(testEngine, signedManager, battleKey, ALICE, uint16(1)); assertEq( testEngine.getPlayerSwitchForTurnFlagForBattleState(battleKey), 2, @@ -502,7 +502,7 @@ contract EngineOptimizationTest is Test, BattleHelper { uint256 turnBefore = testEngine.getTurnIdForBattleState(battleKey); _commitRevealExecuteForAliceAndBob( - testEngine, signedManager, battleKey, NO_OP_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + testEngine, signedManager, battleKey, NO_OP_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); assertEq( @@ -514,10 +514,10 @@ contract EngineOptimizationTest is Test, BattleHelper { function _forceP1Switch(Engine testEngine, SignedCommitManager signedManager, bytes32 battleKey) internal { _commitRevealExecuteForAliceAndBob( - testEngine, signedManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + testEngine, signedManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); _commitRevealExecuteForAliceAndBob( - testEngine, signedManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + testEngine, signedManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); assertEq(testEngine.getPlayerSwitchForTurnFlagForBattleState(battleKey), 1, "P1 should be forced to switch"); @@ -528,10 +528,10 @@ contract EngineOptimizationTest is Test, BattleHelper { SignedCommitManager signedManager, bytes32 battleKey, address player, - uint240 monIndex + uint16 monIndex ) internal { vm.prank(player); - signedManager.executeSinglePlayerMove(battleKey, SWITCH_MOVE_INDEX, bytes32(0), monIndex); + signedManager.executeSinglePlayerMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), monIndex); testEngine.resetCallContext(); } @@ -665,7 +665,7 @@ contract EngineOptimizationTest is Test, BattleHelper { ); vm.warp(vm.getBlockTimestamp() + 1); _commitRevealExecuteForAliceAndBob( - engine, commitManager, warmupKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, warmupKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); _commitRevealExecuteForAliceAndBob(engine, commitManager, warmupKey, 0, 0, 0, 0); _commitRevealExecuteForAliceAndBob(engine, commitManager, warmupKey, NO_OP_MOVE_INDEX, NO_OP_MOVE_INDEX, 0, 0); @@ -683,7 +683,7 @@ contract EngineOptimizationTest is Test, BattleHelper { ); vm.warp(vm.getBlockTimestamp() + 1); _commitRevealExecuteForAliceAndBob( - engine, commitManager, externalKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, externalKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); vm.startSnapshotGas("ExternalStaminaRegen"); @@ -704,7 +704,7 @@ contract EngineOptimizationTest is Test, BattleHelper { ); vm.warp(vm.getBlockTimestamp() + 1); _commitRevealExecuteForAliceAndBob( - engine, commitManager, inlineKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, inlineKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); vm.startSnapshotGas("InlineStaminaRegen"); diff --git a/test/EngineTest.sol b/test/EngineTest.sol index 3579cdfa..7ff48a80 100644 --- a/test/EngineTest.sol +++ b/test/EngineTest.sol @@ -62,9 +62,9 @@ contract EngineTest is Test, BattleHelper { Mon dummyMon; IMoveSet dummyAttack; - // Helper to pack ForceSwitchMove extraData: lower 120 bits = playerIndex, upper 120 bits = monToSwitchIndex - function _packForceSwitch(uint256 playerIndex, uint256 monToSwitchIndex) internal pure returns (uint240) { - return uint240(playerIndex | (monToSwitchIndex << 120)); + // Pack ForceSwitchMove extraData: bit 0 = playerIndex (0/1), bits 1..15 = monToSwitchIndex + function _packForceSwitch(uint256 playerIndex, uint256 monToSwitchIndex) internal pure returns (uint16) { + return uint16((playerIndex & 0x1) | (monToSwitchIndex << 1)); } function setUp() public { @@ -123,7 +123,7 @@ contract EngineTest is Test, BattleHelper { // Let Alice and Bob both commit to switching _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Let Alice and Bob do a no-op @@ -190,7 +190,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Let Alice and Bob commit and reveal to both choosing attack (move index 0) @@ -266,7 +266,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Let Alice and Bob commit and reveal to both choosing attack (move index 0) @@ -348,7 +348,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Let Alice and Bob commit and reveal to both choosing attack (move index 0) @@ -374,7 +374,7 @@ contract EngineTest is Test, BattleHelper { // Reveal Alice's move, and advance game state vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, bytes32(""), uint240(1), false); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(1), false); engine.execute(battleKey); engine.resetCallContext(); @@ -400,7 +400,7 @@ contract EngineTest is Test, BattleHelper { // Attempt to reveal Alice's move, and assert that we cannot advance the game state vm.startPrank(ALICE); vm.expectRevert(abi.encodeWithSignature("InvalidMove(address)", ALICE)); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, bytes32(""), uint240(0), false); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(0), false); // Attempt to forcibly advance the game state vm.expectRevert(); @@ -476,7 +476,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Let Alice and Bob commit and reveal to both choosing attack (move index 0) @@ -534,11 +534,11 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Second move, have Alice swap out to mon at index 1, have Bob use attack - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), 0); // Assert that mon index for Alice is 1 // Assert that the mon state for Alice has -5 applied to the switched in mon @@ -587,11 +587,11 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Second move, have Alice swap out to mon at index 1, have Bob use fast attack - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), 0); // Assert that mon index for Alice is 1 // Assert that the mon state for Alice has -5 applied to the previous mon @@ -640,11 +640,11 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Second move, have Alice swap out to mon at index 1, have Bob use fast attack which supersedes Switch - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), 0); // Given that it's a KO (even though Alice chose switch), // check that now they have the priority flag again @@ -699,7 +699,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Let Alice and Bob commit and reveal to both choosing attack (move index 0) @@ -752,7 +752,7 @@ contract EngineTest is Test, BattleHelper { // Select mons _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both attack (costs 1 stamina each) @@ -829,7 +829,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Let Alice and Bob commit and reveal to both choosing attack (move index 0) @@ -900,19 +900,19 @@ contract EngineTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validator, engine, defaultOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Commit move index 0 for Bob uint8 moveIndex = 0; vm.startPrank(BOB); - bytes32 bobMoveHash = keccak256(abi.encodePacked(moveIndex, bytes32(""), uint240(0))); + bytes32 bobMoveHash = keccak256(abi.encodePacked(moveIndex, uint104(0), uint16(0))); commitManager.commitMove(battleKey, bobMoveHash); // Assert that Alice cannot reveal anything because of the stamina cost (she has the high stamina cost mon) vm.startPrank(ALICE); vm.expectRevert(abi.encodeWithSignature("InvalidMove(address)", ALICE)); - commitManager.revealMove(battleKey, moveIndex, bytes32(""), uint240(0), false); + commitManager.revealMove(battleKey, moveIndex, uint104(0), uint16(0), false); } // Ensure that we cannot write to mon state when there is no active execute() call in the call stack @@ -997,13 +997,13 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both player pick move index 0, which for Bob afflicts the instant death condition on the // opposing mon (Alice's) and knocks it out uint8 moveIndex = 0; - uint240 extraData = 0; + uint16 extraData = 0; _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, moveIndex, moveIndex, extraData, extraData); // Assert Bob wins @@ -1076,20 +1076,20 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both player pick move index 0, which for Bob afflicts the instant death condition on the // opposing mon (Alice's) and knocks it out uint8 moveIndex = 0; - uint240 extraData = 0; + uint16 extraData = 0; _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, moveIndex, moveIndex, extraData, extraData); // Now only Alice should be able to switch vm.startPrank(ALICE); // Alice should be able to reveal because she is the only player (player flag should be set) - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, bytes32(""), uint240(1), false); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(1), false); // Execute the switch engine.execute(battleKey); @@ -1135,13 +1135,13 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice swaps to mon index 1, and Bob applies the effect // The effect should be applied to mon index 1 for Alice but only during the duration of the turn // (We have a check for 2 instead of 1 to avoid confusing it with the base case state) - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), 0); // Assert that the temporary stat boost effect is updated to 2 because the roundEnd hook also runs assertEq(engine.getMonStateForBattle(battleKey, 0, 1, MonStateIndexName.Attack), 2); @@ -1209,14 +1209,14 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both player pick move index 0, which for Bob afflicts the instant death condition on the // opposing mon (Alice's) // But Alice's mon should KO Bob's before the end of round takes place uint8 moveIndex = 0; - uint240 extraData = 0; + uint16 extraData = 0; _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, moveIndex, moveIndex, extraData, extraData); // Assert Alice wins @@ -1289,26 +1289,26 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both player pick move index 0, which for Bob afflicts the instant death condition on the // opposing mon (Alice's) and knocks it out // But Bob moves first (higher priority), so he gets the instant death affliction uint8 moveIndex = 0; - uint240 extraData = 0; + uint16 extraData = 0; _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, moveIndex, moveIndex, extraData, extraData); // Now if Alice tries to pick a non-switch move, the engine should revert vm.startPrank(ALICE); - bytes32 salt = ""; + uint104 salt = 0; uint8 aliceMoveIndex = 0; bytes32 aliceMoveHash = keccak256(abi.encodePacked(aliceMoveIndex, salt, extraData)); commitManager.commitMove(battleKey, aliceMoveHash); // Bob reveals a swap vm.startPrank(BOB); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, uint240(1), false); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, uint16(1), false); // Alice's reveal will revert (must choose switch) vm.startPrank(ALICE); @@ -1382,19 +1382,19 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both player pick move index 0, which for Bob afflicts the instant death condition on the // opposing mon (Alice's) and knocks it out // But Bob moves first (higher priority), so he gets the instant death affliction uint8 moveIndex = 0; - uint240 extraData = 0; + uint16 extraData = 0; _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, moveIndex, moveIndex, extraData, extraData); // Now both moves have to swap to index 1 for their mons _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(1), uint240(1) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(1), uint16(1) ); } @@ -1456,7 +1456,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both player pick move index 0 @@ -1464,7 +1464,7 @@ contract EngineTest is Test, BattleHelper { // Alice tries to go fast for a lethal effect // Bob should win priority and inflict skip turn effect uint8 moveIndex = 0; - uint240 extraData = 0; + uint16 extraData = 0; _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, moveIndex, moveIndex, extraData, extraData); // Assert no winner, and no damage dealt @@ -1538,7 +1538,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both player pick move index 0, but Alice encodes a swap to mon index 1 for player index 1 (Bob) @@ -1617,7 +1617,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both player pick move index 0, but Alice encodes a swap to mon index 1 for player index 0 (Alice) @@ -1696,12 +1696,12 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Let Bob commit and reveal to attack (move index 0) - uint240 extraData = 0; - bytes32 salt = ""; + uint16 extraData = 0; + uint104 salt = 0; uint8 moveIndex = 0; vm.startPrank(BOB); commitManager.commitMove(battleKey, keccak256(abi.encodePacked(moveIndex, salt, extraData))); @@ -1788,12 +1788,12 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Let Bob commit and reveal to attack (move index 0) - uint240 extraData = 0; - bytes32 salt = ""; + uint16 extraData = 0; + uint104 salt = 0; uint8 moveIndex = 0; vm.startPrank(BOB); commitManager.commitMove(battleKey, keccak256(abi.encodePacked(moveIndex, salt, extraData))); @@ -1885,7 +1885,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Let both players select move index 0 @@ -1955,7 +1955,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // (Have Alice force Bob to switch to mon index 1, have Bob select the instant death switch in effect @@ -2024,12 +2024,12 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Have Alice switch to their second mon, have Bob select the instant death switch in effect // (But swapping to mon index 1 for Alice will trigger on switch in and kill the mon) - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), 0); // Assert that the player switch for turn flag is now 0, indicating Alice has to switch (, BattleData memory state) = engine.getBattle(battleKey); @@ -2099,7 +2099,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // After this, Alice's mon should be dead and Bob should be the winner @@ -2193,7 +2193,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice switches themselves to mon index 1, while Bob chooses move index 0 @@ -2293,11 +2293,11 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice switches themselves to mon index 1, while Bob chooses move index 0 - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), 0); // Assert that Alice's new mon is now KOed assertEq(engine.getMonStateForBattle(battleKey, 0, 1, MonStateIndexName.IsKnockedOut), 1); @@ -2349,7 +2349,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice and Bob both select attacks (they should apply the single instance effect on hit) @@ -2395,19 +2395,19 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Bob commits to NO_OP vm.startPrank(BOB); uint8 moveIndex = NO_OP_MOVE_INDEX; - bytes32 bobMoveHash = keccak256(abi.encodePacked(moveIndex, bytes32(""), uint240(0))); + bytes32 bobMoveHash = keccak256(abi.encodePacked(moveIndex, uint104(0), uint16(0))); commitManager.commitMove(battleKey, bobMoveHash); // Alice should revert when revealing vm.startPrank(ALICE); vm.expectRevert(abi.encodeWithSignature("InvalidMove(address)", ALICE)); - commitManager.revealMove(battleKey, 0, bytes32(""), uint240(0), false); + commitManager.revealMove(battleKey, 0, uint104(0), uint16(0), false); } function test_onMonSwitchOutHookWorksWithTempStatBoost() public { @@ -2451,7 +2451,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice and Bob both select attacks (they should apply the temporary stat boost effect) @@ -2463,7 +2463,7 @@ contract EngineTest is Test, BattleHelper { // Alice and Bob both switch to mon index 1 _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(1), uint240(1) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(1), uint16(1) ); // Assert that the temporary stat boost effect was removed from both mons @@ -2517,7 +2517,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice and Bob both select attacks, both of them are move index 0 (do damage rebound) @@ -2568,8 +2568,8 @@ contract EngineTest is Test, BattleHelper { bytes32 battleKey = _startBattle(twoMoveValidator, engine, defaultOracle, defaultRegistry, matchmaker, address(commitManager)); // Alice commits to swapping in mon index 1 - bytes32 salt = ""; - bytes32 aliceMoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint240(1))); + uint104 salt = 0; + bytes32 aliceMoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint16(1))); vm.startPrank(ALICE); commitManager.commitMove(battleKey, aliceMoveHash); @@ -2579,19 +2579,19 @@ contract EngineTest is Test, BattleHelper { // Bob reveals vm.startPrank(BOB); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, uint240(1), false); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, uint16(1), false); // Bob cannot reveal twice vm.expectRevert(DefaultCommitManager.AlreadyRevealed.selector); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, uint240(1), false); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, uint16(1), false); // Alice reveals but does not execute vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, uint240(1), false); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, uint16(1), false); // Second reveal should also fail vm.expectRevert(DefaultCommitManager.AlreadyRevealed.selector); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, uint240(1), false); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, uint16(1), false); } function test_cannotCommitToEndedBattle() public { @@ -2622,7 +2622,7 @@ contract EngineTest is Test, BattleHelper { // Both players send in mon index 0 _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Bob does nothing (it's his turn to move, so he'll lose by timeout) @@ -2671,7 +2671,7 @@ contract EngineTest is Test, BattleHelper { // Alice commits to switch to mon index 0 vm.startPrank(ALICE); - commitManager.commitMove(battleKey, keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(""), uint240(0)))); + commitManager.commitMove(battleKey, keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, uint104(0), uint16(0)))); // Attempt to end the battle immediately (same block as start) // Bob hasn't committed and timeout is 0, so Bob loses, but game should revert @@ -2722,10 +2722,10 @@ contract EngineTest is Test, BattleHelper { bytes32 battleKey, uint8 aliceMoveIndex, uint8 bobMoveIndex, - uint240 aliceExtraData, - uint240 bobExtraData + uint16 aliceExtraData, + uint16 bobExtraData ) internal { - bytes32 salt = ""; + uint104 salt = 0; bytes32 aliceMoveHash = keccak256(abi.encodePacked(aliceMoveIndex, salt, aliceExtraData)); bytes32 bobMoveHash = keccak256(abi.encodePacked(bobMoveIndex, salt, bobExtraData)); // Decide which player commits @@ -2830,8 +2830,8 @@ contract EngineTest is Test, BattleHelper { */ function test_turn0DefaultCommitManagerValidPreimage() public { bytes32 battleKey = _startDummyBattleWithTwoMons(); - bytes32 salt = ""; - uint240 extraData = uint240(0); + uint104 salt = 0; + uint16 extraData = uint16(0); bytes32 moveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, extraData)); // Ensure Alice cannot reveal yet because Alice has not yet committed @@ -2889,8 +2889,8 @@ contract EngineTest is Test, BattleHelper { bytes32 battleKey = _startDummyBattleWithTwoMons(); // Let Alice commit to choosing switch - bytes32 salt = ""; - uint240 extraData = uint240(0); + uint104 salt = 0; + uint16 extraData = uint16(0); bytes32 moveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, extraData)); // Let Alice commit the first move (switching in mon index 0) @@ -2910,7 +2910,7 @@ contract EngineTest is Test, BattleHelper { commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, extraData, true); engine.resetCallContext(); // New turn, both players swap to mon index 1 - extraData = uint240(1); + extraData = uint16(1); moveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, extraData)); // Ensure Bob cannot reveal yet because Bob has not yet committed @@ -2936,7 +2936,7 @@ contract EngineTest is Test, BattleHelper { // Let Alice reveal their invalid move index of 0 vm.startPrank(ALICE); vm.expectRevert(abi.encodeWithSignature("InvalidMove(address)", ALICE)); - uint240 invalidExtraData = uint240(0); + uint16 invalidExtraData = uint16(0); commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, invalidExtraData, true); engine.resetCallContext(); // Now let Alice reveal a valid move @@ -2971,8 +2971,8 @@ contract EngineTest is Test, BattleHelper { bytes32 battleKey = _startDummyBattleWithTwoMons(); // Let Alice commit to choosing switch - bytes32 salt = ""; - uint240 extraData = uint240(0); + uint104 salt = 0; + uint16 extraData = uint16(0); bytes32 moveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, extraData)); vm.startPrank(ALICE); commitManager.commitMove(battleKey, moveHash); @@ -2991,8 +2991,8 @@ contract EngineTest is Test, BattleHelper { bytes32 battleKey = _startDummyBattleWithTwoMons(); // Let Alice commit to choosing switch - bytes32 salt = ""; - uint240 extraData = uint240(0); + uint104 salt = 0; + uint16 extraData = uint16(0); bytes32 moveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, extraData)); vm.startPrank(ALICE); commitManager.commitMove(battleKey, moveHash); @@ -3067,7 +3067,7 @@ contract EngineTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Let Alice and Bob commit and reveal to both choosing attack (move index 0) @@ -3111,9 +3111,9 @@ contract EngineTest is Test, BattleHelper { ); bytes32 battleKey = _startBattle(validatorToUse, engine, defaultOracle, defaultRegistry, matchmaker, address(commitManager)); // Alice sends in mon index 0, Bob sends in the fast mon - _commitRevealExecuteForAliceAndBob(battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(1)); + _commitRevealExecuteForAliceAndBob(battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(1)); // Both players pick move index 0 - _commitRevealExecuteForAliceAndBob(battleKey, 0, 0, uint240(1), uint240(0)); + _commitRevealExecuteForAliceAndBob(battleKey, 0, 0, uint16(1), uint16(0)); // Switch for turn flag should be 0, Bob's active mon index should now be 0 (, BattleData memory state) = engine.getBattle(battleKey); assertEq(state.playerSwitchForTurnFlag, 0); @@ -3139,15 +3139,15 @@ contract EngineTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validatorToUse, engine, defaultOracle, defaultRegistry, matchmaker, address(commitManager)); // Alice and Bob send in their first mon - _commitRevealExecuteForAliceAndBob(battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForAliceAndBob(battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); // Verify the dummy effect is applied to Alice's mon (EffectInstance[] memory effects, uint256[] memory indices) = engine.getEffects(battleKey, 1, 0); assertEq(effects.length, 1); // Alice uses the edit effect attack to change the extra data to 69 on Bob - // Pack extraData: lower 80 bits = targetIndex (1), next 80 bits = monIndex (0), upper 80 bits = effectIndex - uint240 editExtraData = uint240(1 | (0 << 80) | (indices[0] << 160)); + // Pack extraData: bits 0..1 = targetIndex (1), bits 2..5 = monIndex (0), bits 6..15 = effectIndex + uint16 editExtraData = uint16(uint256(1) | (uint256(0) << 2) | (uint256(indices[0]) << 6)); _commitRevealExecuteForAliceAndBob(battleKey, 0, NO_OP_MOVE_INDEX, editExtraData, 0); (effects, ) = engine.getEffects(battleKey, 1, 0); assertEq(effects[0].data, bytes32(uint256(69))); diff --git a/test/GachaTest.sol b/test/GachaTest.sol index ba3af067..f116e144 100644 --- a/test/GachaTest.sol +++ b/test/GachaTest.sol @@ -125,7 +125,7 @@ contract GachaTest is Test, BattleHelper { // Alice commits switching to mon index 0 vm.startPrank(ALICE); - commitManager.commitMove(battleKey, keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(""), uint240(0)))); + commitManager.commitMove(battleKey, keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(""), uint16(0)))); // Alice wins the battle (inactivity for Bob), we skip ahead mockRNG.setRNG(1); // No extra bonus for points @@ -197,7 +197,7 @@ contract GachaTest is Test, BattleHelper { // Alice commits switching to mon index 0 vm.startPrank(ALICE); - commitManager.commitMove(battleKey, keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(""), uint240(0)))); + commitManager.commitMove(battleKey, keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(""), uint16(0)))); // Alice wins the battle engine.end(battleKey); @@ -295,7 +295,7 @@ contract GachaTest is Test, BattleHelper { _startBattle(validator, engine, defaultOracle, defaultRegistry, matchmaker, hooks, address(commitManager)); vm.warp(vm.getBlockTimestamp() + 1); vm.startPrank(ALICE); - commitManager.commitMove(battleKey, keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(""), uint240(0)))); + commitManager.commitMove(battleKey, keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(""), uint16(0)))); vm.stopPrank(); mockRNG.setRNG(1); engine.end(battleKey); @@ -318,7 +318,7 @@ contract GachaTest is Test, BattleHelper { _startBattle(validator, engine, defaultOracle, defaultRegistry, matchmaker, hooks, address(commitManager)); vm.warp(vm.getBlockTimestamp() + 1); vm.startPrank(ALICE); - commitManager.commitMove(battleKey, keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(""), uint240(0)))); + commitManager.commitMove(battleKey, keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(""), uint16(0)))); vm.stopPrank(); mockRNG.setRNG(1); engine.end(battleKey); diff --git a/test/InlineAbilityParityTest.sol b/test/InlineAbilityParityTest.sol index 7320fff7..0bbf6940 100644 --- a/test/InlineAbilityParityTest.sol +++ b/test/InlineAbilityParityTest.sol @@ -117,7 +117,7 @@ contract InlineAbilityParityTest is Test, BattleHelper { // Turn 0: both switch in _commitRevealExecuteForAliceAndBob( engine, commitManager, battleKey, - SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); assertTrue(_hasEffect(battleKey, 0, 0, address(singletonAbility)), "p0 should have effect"); @@ -139,18 +139,18 @@ contract InlineAbilityParityTest is Test, BattleHelper { // Turn 0: switch in mon 0 _commitRevealExecuteForAliceAndBob( engine, commitManager, battleKey, - SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); assertEq(_countEffect(battleKey, 0, 0, address(singletonAbility)), 1, "should have 1 effect"); // Switch to mon 1 then back to mon 0 _commitRevealExecuteForAliceAndBob( engine, commitManager, battleKey, - SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(1), 0 + SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(1), 0 ); _commitRevealExecuteForAliceAndBob( engine, commitManager, battleKey, - SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(0), 0 + SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(0), 0 ); assertEq(_countEffect(battleKey, 0, 0, address(singletonAbility)), 1, "should still have exactly 1 effect"); @@ -170,7 +170,7 @@ contract InlineAbilityParityTest is Test, BattleHelper { // Turn 0 _commitRevealExecuteForAliceAndBob( engine, commitManager, battleKey, - SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Execute 2 attack turns — AfterDamage should increment counter each time @@ -205,7 +205,7 @@ contract InlineAbilityParityTest is Test, BattleHelper { // Turn 0 _commitRevealExecuteForAliceAndBob( engine, commitManager, battleKey, - SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // EffectAbility adds dummyEffect — verify it's registered @@ -233,14 +233,14 @@ contract InlineAbilityParityTest is Test, BattleHelper { // Turn 0: switch in mon 0 (inline) _commitRevealExecuteForAliceAndBob( engine, commitManager, battleKey, - SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); assertTrue(_hasEffect(battleKey, 0, 0, address(singletonAbility)), "alice mon 0 should have inline effect"); // Alice switches to mon 1 (external) _commitRevealExecuteForAliceAndBob( engine, commitManager, battleKey, - SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(1), 0 + SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(1), 0 ); assertTrue(_hasEffect(battleKey, 0, 1, address(dummyEffect)), "alice mon 1 should have external effect"); } @@ -260,7 +260,7 @@ contract InlineAbilityParityTest is Test, BattleHelper { _commitRevealExecuteForAliceAndBob( engine, commitManager, battleKey, - SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); assertTrue(_hasEffect(battleKey, 0, 0, address(singletonAbility)), "p0 should have inline effect"); diff --git a/test/InlineEngineGasTest.sol b/test/InlineEngineGasTest.sol index 579b6e95..19842928 100644 --- a/test/InlineEngineGasTest.sol +++ b/test/InlineEngineGasTest.sol @@ -15,7 +15,7 @@ import {SignedCommitManager} from "../src/commit-manager/SignedCommitManager.sol import {Engine} from "../src/Engine.sol"; import {IEngine} from "../src/IEngine.sol"; import {IValidator} from "../src/IValidator.sol"; -import {EIP712} from "../src/lib/EIP712.sol"; +import {SignedCommitHelper} from "./abstract/SignedCommitHelper.sol"; import {IEffect} from "../src/effects/IEffect.sol"; import {StaminaRegen} from "../src/effects/StaminaRegen.sol"; @@ -60,10 +60,6 @@ contract InlineEngineGasTest is Test, BattleHelper { uint256 constant MONS_PER_TEAM = 4; uint256 constant MOVES_PER_MON = 4; - function _packStatBoost(uint256 playerIndex, uint256 monIndex, uint256 statIndex, int32 boostAmount) internal pure returns (uint240) { - return uint240(playerIndex | (monIndex << 60) | (statIndex << 120) | (uint256(uint32(boostAmount)) << 180)); - } - function setUp() public { defaultOracle = new DefaultRandomnessOracle(); // Create engine with inline validation defaults @@ -162,26 +158,26 @@ contract InlineEngineGasTest is Test, BattleHelper { vm.warp(vm.getBlockTimestamp() + 1); vm.startSnapshotGas("FirstBattle"); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, 1, 0, 0); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 2, uint240(1), _packStatBoost(1, 0, uint256(MonStateIndexName.Attack), int32(90))); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 2, uint16(1), _packStatBoost(1, 0, uint256(MonStateIndexName.Attack), int32(90))); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 2, 3, _packStatBoost(0, 1, uint256(MonStateIndexName.Attack), int32(90)), 0); vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(0), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(0), true); engine.resetCallContext(); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 2, NO_OP_MOVE_INDEX, _packStatBoost(0, 0, uint256(MonStateIndexName.Attack), int32(90)), 0); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 3, NO_OP_MOVE_INDEX, 0, 0); vm.startPrank(BOB); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), true); engine.resetCallContext(); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 2, 0, _packStatBoost(1, 1, uint256(MonStateIndexName.Attack), int32(90))); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 3, 0, 0); vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(2), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(2), true); engine.resetCallContext(); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 3, 0, 0); vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(3), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(3), true); engine.resetCallContext(); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 3, 0, 0); uint256 firstBattleGas = vm.stopSnapshotGas("FirstBattle"); @@ -209,27 +205,27 @@ contract InlineEngineGasTest is Test, BattleHelper { console.log("After setup 2 - packedP1EffectsCount:", cfgAfterSetup2.packedP1EffectsCount); vm.startSnapshotGas("SecondBattle"); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 3, 1, _packStatBoost(0, 0, uint256(MonStateIndexName.Attack), int32(90)), 0); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 0, NO_OP_MOVE_INDEX, 0, 0); vm.startPrank(BOB); - commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint240(1), true); + commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint16(1), true); engine.resetCallContext(); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, 2, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, SWITCH_MOVE_INDEX, 2, uint16(1), 0); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 3, NO_OP_MOVE_INDEX, _packStatBoost(0, 1, uint256(MonStateIndexName.Attack), int32(90)), 0); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 0, NO_OP_MOVE_INDEX, 0, 0); vm.startPrank(BOB); - commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint240(2), true); + commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint16(2), true); engine.resetCallContext(); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, NO_OP_MOVE_INDEX, 3, 0, _packStatBoost(1, 2, uint256(MonStateIndexName.Attack), int32(90))); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, NO_OP_MOVE_INDEX, 0, 0, 0); vm.startPrank(ALICE); - commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint240(2), true); + commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint16(2), true); engine.resetCallContext(); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 3, 3, _packStatBoost(0, 2, uint256(MonStateIndexName.Attack), int32(90)), _packStatBoost(1, 2, uint256(MonStateIndexName.Attack), int32(90))); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 0, NO_OP_MOVE_INDEX, 0, 0); vm.startPrank(BOB); - commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint240(3), true); + commitManager.revealMove(battleKey2, SWITCH_MOVE_INDEX, 0, uint16(3), true); engine.resetCallContext(); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey2, 0, NO_OP_MOVE_INDEX, 0, 0); uint256 secondBattleGas = vm.stopSnapshotGas("SecondBattle"); @@ -252,26 +248,26 @@ contract InlineEngineGasTest is Test, BattleHelper { vm.warp(vm.getBlockTimestamp() + 1); vm.startSnapshotGas("ThirdBattle"); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, 0, 1, 0, 0); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, SWITCH_MOVE_INDEX, 2, uint240(1), _packStatBoost(1, 0, uint256(MonStateIndexName.Attack), int32(90))); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, SWITCH_MOVE_INDEX, 2, uint16(1), _packStatBoost(1, 0, uint256(MonStateIndexName.Attack), int32(90))); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, 2, 3, _packStatBoost(0, 1, uint256(MonStateIndexName.Attack), int32(90)), 0); vm.startPrank(ALICE); - commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint240(0), true); + commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint16(0), true); engine.resetCallContext(); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, 2, NO_OP_MOVE_INDEX, _packStatBoost(0, 0, uint256(MonStateIndexName.Attack), int32(90)), 0); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, 3, NO_OP_MOVE_INDEX, 0, 0); vm.startPrank(BOB); - commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint240(1), true); + commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint16(1), true); engine.resetCallContext(); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, NO_OP_MOVE_INDEX, 2, 0, _packStatBoost(1, 1, uint256(MonStateIndexName.Attack), int32(90))); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, NO_OP_MOVE_INDEX, 3, 0, 0); vm.startPrank(ALICE); - commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint240(2), true); + commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint16(2), true); engine.resetCallContext(); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, NO_OP_MOVE_INDEX, 3, 0, 0); vm.startPrank(ALICE); - commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint240(3), true); + commitManager.revealMove(battleKey3, SWITCH_MOVE_INDEX, 0, uint16(3), true); engine.resetCallContext(); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey3, NO_OP_MOVE_INDEX, 3, 0, 0); uint256 thirdBattleGas = vm.stopSnapshotGas("ThirdBattle"); @@ -338,7 +334,7 @@ contract InlineEngineGasTest is Test, BattleHelper { vm.warp(vm.getBlockTimestamp() + 1); vm.startSnapshotGas("Battle1_Execute"); - _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey1, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey1, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey1, 0, 0, 0, 0); uint256 execute1 = vm.stopSnapshotGas("Battle1_Execute"); @@ -350,7 +346,7 @@ contract InlineEngineGasTest is Test, BattleHelper { vm.warp(vm.getBlockTimestamp() + 1); vm.startSnapshotGas("Battle2_Execute"); - _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey2, 0, 0, 0, 0); uint256 execute2 = vm.stopSnapshotGas("Battle2_Execute"); @@ -412,7 +408,7 @@ contract InlineEngineGasTest is Test, BattleHelper { vm.warp(vm.getBlockTimestamp() + 1); vm.startSnapshotGas("B1_Execute"); - _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey1, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey1, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); (BattleConfigView memory cfgAfterSwitch,) = inlineEngine.getBattle(battleKey1); console.log("After B1 switch - globalEffectsLength:", cfgAfterSwitch.globalEffectsLength); @@ -445,7 +441,7 @@ contract InlineEngineGasTest is Test, BattleHelper { console.log("After B2 setup - packedP1EffectsCount:", cfg2.packedP1EffectsCount); vm.startSnapshotGas("B2_Execute"); - _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey2, 0, 0, 0, 0); _commitRevealExecuteForEngine(inlineEngine, inlineCommitManager, battleKey2, 1, 1, 0, 0); uint256 execute2 = vm.stopSnapshotGas("B2_Execute"); @@ -525,10 +521,10 @@ contract InlineEngineGasTest is Test, BattleHelper { bytes32 battleKey, uint8 aliceMoveIndex, uint8 bobMoveIndex, - uint240 aliceExtraData, - uint240 bobExtraData + uint16 aliceExtraData, + uint16 bobExtraData ) internal { - bytes32 salt = ""; + uint104 salt = 0; bytes32 aliceMoveHash = keccak256(abi.encodePacked(aliceMoveIndex, salt, aliceExtraData)); bytes32 bobMoveHash = keccak256(abi.encodePacked(bobMoveIndex, salt, bobExtraData)); uint256 turnId = eng.getTurnIdForBattleState(battleKey); @@ -563,7 +559,7 @@ contract InlineEngineGasTest is Test, BattleHelper { /// SignedMatchmaker (no propose/accept/confirm storage), and /// SignedCommitManager::executeWithDualSignedMoves (1 TX per two-player turn). /// @dev Forced single-player switches after KOs use SignedCommitManager::executeSinglePlayerMove. -contract FullyOptimizedInlineGasTest is Test, BattleHelper, EIP712 { +contract FullyOptimizedInlineGasTest is BattleHelper, SignedCommitHelper { uint256 constant MONS_PER_TEAM = 4; uint256 constant MOVES_PER_MON = 4; @@ -584,14 +580,6 @@ contract FullyOptimizedInlineGasTest is Test, BattleHelper, EIP712 { mapping(bytes32 => bool) private _seenSlot; bytes32[] private _seenKeys; - function _domainNameAndVersion() internal pure override returns (string memory, string memory) { - return ("SignedCommitManager", "1"); - } - - function _packStatBoost(uint256 playerIndex, uint256 monIndex, uint256 statIndex, int32 boostAmount) internal pure returns (uint240) { - return uint240(playerIndex | (monIndex << 60) | (statIndex << 120) | (uint256(uint32(boostAmount)) << 180)); - } - function setUp() public { p0 = vm.addr(P0_PK); p1 = vm.addr(P1_PK); @@ -645,39 +633,6 @@ contract FullyOptimizedInlineGasTest is Test, BattleHelper, EIP712 { return battleKey; } - function _signDualReveal( - uint256 privateKey, - bytes32 battleKey, - uint64 turnId, - bytes32 committerMoveHash, - uint8 revealerMoveIndex, - bytes32 revealerSalt, - uint240 revealerExtraData - ) internal view returns (bytes memory) { - bytes32 domainSeparator = keccak256( - abi.encode( - _DOMAIN_TYPEHASH, - keccak256("SignedCommitManager"), - keccak256("1"), - block.chainid, - address(signedCommitManager) - ) - ); - bytes32 structHash = SignedCommitLib.hashDualSignedReveal( - SignedCommitLib.DualSignedReveal({ - battleKey: battleKey, - turnId: turnId, - committerMoveHash: committerMoveHash, - revealerMoveIndex: revealerMoveIndex, - revealerSalt: revealerSalt, - revealerExtraData: revealerExtraData - }) - ); - bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); - return abi.encodePacked(r, s, v); - } - /// @dev Executes a two-player turn in 1 TX via executeWithDualSignedMoves. /// p0Move/p1Move semantics match _commitRevealExecuteForAliceAndBob so the /// battle scripts can be transcribed directly from the non-optimized test. @@ -685,17 +640,18 @@ contract FullyOptimizedInlineGasTest is Test, BattleHelper, EIP712 { bytes32 battleKey, uint8 p0MoveIndex, uint8 p1MoveIndex, - uint240 p0ExtraData, - uint240 p1ExtraData + uint16 p0ExtraData, + uint16 p1ExtraData ) internal { uint64 turnId = uint64(engine.getTurnIdForBattleState(battleKey)); - bytes32 committerSalt = keccak256(abi.encode("committer", battleKey, turnId)); - bytes32 revealerSalt = keccak256(abi.encode("revealer", battleKey, turnId)); + uint104 committerSalt = uint104(uint256(keccak256(abi.encode("committer", battleKey, turnId)))); + uint104 revealerSalt = uint104(uint256(keccak256(abi.encode("revealer", battleKey, turnId)))); uint8 committerMoveIndex; - uint240 committerExtraData; + uint16 committerExtraData; uint8 revealerMoveIndex; - uint240 revealerExtraData; + uint16 revealerExtraData; + uint256 committerPk; uint256 revealerPk; address committer; @@ -704,6 +660,7 @@ contract FullyOptimizedInlineGasTest is Test, BattleHelper, EIP712 { committerExtraData = p0ExtraData; revealerMoveIndex = p1MoveIndex; revealerExtraData = p1ExtraData; + committerPk = P0_PK; revealerPk = P1_PK; committer = p0; } else { @@ -711,14 +668,17 @@ contract FullyOptimizedInlineGasTest is Test, BattleHelper, EIP712 { committerExtraData = p1ExtraData; revealerMoveIndex = p0MoveIndex; revealerExtraData = p0ExtraData; + committerPk = P1_PK; revealerPk = P0_PK; committer = p1; } bytes32 committerMoveHash = keccak256(abi.encodePacked(committerMoveIndex, committerSalt, committerExtraData)); + address mgr = address(signedCommitManager); + bytes memory committerSig = _signCommit(mgr, committerPk, committerMoveHash, battleKey, turnId); bytes memory revealerSig = _signDualReveal( - revealerPk, battleKey, turnId, committerMoveHash, + mgr, revealerPk, battleKey, turnId, committerMoveHash, revealerMoveIndex, revealerSalt, revealerExtraData ); @@ -727,16 +687,16 @@ contract FullyOptimizedInlineGasTest is Test, BattleHelper, EIP712 { battleKey, committerMoveIndex, committerSalt, committerExtraData, revealerMoveIndex, revealerSalt, revealerExtraData, - revealerSig + committerSig, revealerSig ); engine.resetCallContext(); } /// @dev Single-player forced switch after a KO. This uses the optimized /// SignedCommitManager path because there is no hidden opponent move to reveal. - function _fastSwitchReveal(bytes32 battleKey, bool isP0, uint240 extraData) internal { + function _fastSwitchReveal(bytes32 battleKey, bool isP0, uint16 extraData) internal { vm.prank(isP0 ? p0 : p1); - signedCommitManager.executeSinglePlayerMove(battleKey, SWITCH_MOVE_INDEX, bytes32(0), extraData); + signedCommitManager.executeSinglePlayerMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), extraData); engine.resetCallContext(); } @@ -768,43 +728,43 @@ contract FullyOptimizedInlineGasTest is Test, BattleHelper, EIP712 { bytes32 oldFlowBattleKey = _startBattleFullyOptimized(ruleset); vm.warp(vm.getBlockTimestamp() + 1); - _fastTurn(oldFlowBattleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); - _fastTurn(oldFlowBattleKey, 0, NO_OP_MOVE_INDEX, uint240(0), uint240(0)); + _fastTurn(oldFlowBattleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); + _fastTurn(oldFlowBattleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0)); assertEq(engine.getPlayerSwitchForTurnFlagForBattleState(oldFlowBattleKey), 1); vm.prank(p1); uint256 gasBefore = gasleft(); - signedCommitManager.revealMove(oldFlowBattleKey, SWITCH_MOVE_INDEX, bytes32(0), uint240(1), true); + signedCommitManager.revealMove(oldFlowBattleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(1), true); uint256 oldFlowGas = gasBefore - gasleft(); engine.resetCallContext(); - _fastTurn(oldFlowBattleKey, 0, NO_OP_MOVE_INDEX, uint240(0), uint240(0)); + _fastTurn(oldFlowBattleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0)); assertEq(engine.getPlayerSwitchForTurnFlagForBattleState(oldFlowBattleKey), 1); vm.prank(p1); gasBefore = gasleft(); - signedCommitManager.revealMove(oldFlowBattleKey, SWITCH_MOVE_INDEX, bytes32(0), uint240(2), true); + signedCommitManager.revealMove(oldFlowBattleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(2), true); uint256 oldFlowSecondGas = gasBefore - gasleft(); engine.resetCallContext(); bytes32 fastPathBattleKey = _startBattleFullyOptimized(ruleset); vm.warp(vm.getBlockTimestamp() + 1); - _fastTurn(fastPathBattleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); - _fastTurn(fastPathBattleKey, 0, NO_OP_MOVE_INDEX, uint240(0), uint240(0)); + _fastTurn(fastPathBattleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); + _fastTurn(fastPathBattleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0)); assertEq(engine.getPlayerSwitchForTurnFlagForBattleState(fastPathBattleKey), 1); vm.prank(p1); gasBefore = gasleft(); - signedCommitManager.executeSinglePlayerMove(fastPathBattleKey, SWITCH_MOVE_INDEX, bytes32(0), uint240(1)); + signedCommitManager.executeSinglePlayerMove(fastPathBattleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(1)); uint256 fastPathGas = gasBefore - gasleft(); engine.resetCallContext(); - _fastTurn(fastPathBattleKey, 0, NO_OP_MOVE_INDEX, uint240(0), uint240(0)); + _fastTurn(fastPathBattleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0)); assertEq(engine.getPlayerSwitchForTurnFlagForBattleState(fastPathBattleKey), 1); vm.prank(p1); gasBefore = gasleft(); - signedCommitManager.executeSinglePlayerMove(fastPathBattleKey, SWITCH_MOVE_INDEX, bytes32(0), uint240(2)); + signedCommitManager.executeSinglePlayerMove(fastPathBattleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(2)); uint256 fastPathSecondGas = gasBefore - gasleft(); engine.resetCallContext(); @@ -857,19 +817,19 @@ contract FullyOptimizedInlineGasTest is Test, BattleHelper, EIP712 { vm.warp(vm.getBlockTimestamp() + 1); vm.startSnapshotGas("Fast_Battle1"); - _fastTurn(battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _fastTurn(battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); _fastTurn(battleKey, 0, 1, 0, 0); - _fastTurn(battleKey, SWITCH_MOVE_INDEX, 2, uint240(1), _packStatBoost(1, 0, uint256(MonStateIndexName.Attack), int32(90))); + _fastTurn(battleKey, SWITCH_MOVE_INDEX, 2, uint16(1), _packStatBoost(1, 0, uint256(MonStateIndexName.Attack), int32(90))); _fastTurn(battleKey, 2, 3, _packStatBoost(0, 1, uint256(MonStateIndexName.Attack), int32(90)), 0); - _fastSwitchReveal(battleKey, true, uint240(0)); + _fastSwitchReveal(battleKey, true, uint16(0)); _fastTurn(battleKey, 2, NO_OP_MOVE_INDEX, _packStatBoost(0, 0, uint256(MonStateIndexName.Attack), int32(90)), 0); _fastTurn(battleKey, 3, NO_OP_MOVE_INDEX, 0, 0); - _fastSwitchReveal(battleKey, false, uint240(1)); + _fastSwitchReveal(battleKey, false, uint16(1)); _fastTurn(battleKey, NO_OP_MOVE_INDEX, 2, 0, _packStatBoost(1, 1, uint256(MonStateIndexName.Attack), int32(90))); _fastTurn(battleKey, NO_OP_MOVE_INDEX, 3, 0, 0); - _fastSwitchReveal(battleKey, true, uint240(2)); + _fastSwitchReveal(battleKey, true, uint16(2)); _fastTurn(battleKey, NO_OP_MOVE_INDEX, 3, 0, 0); - _fastSwitchReveal(battleKey, true, uint240(3)); + _fastSwitchReveal(battleKey, true, uint16(3)); _fastTurn(battleKey, NO_OP_MOVE_INDEX, 3, 0, 0); uint256 firstBattleGas = vm.stopSnapshotGas("Fast_Battle1"); @@ -891,20 +851,20 @@ contract FullyOptimizedInlineGasTest is Test, BattleHelper, EIP712 { vm.warp(vm.getBlockTimestamp() + 1); vm.startSnapshotGas("Fast_Battle2"); - _fastTurn(battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _fastTurn(battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); _fastTurn(battleKey2, 3, 1, _packStatBoost(0, 0, uint256(MonStateIndexName.Attack), int32(90)), 0); _fastTurn(battleKey2, 0, NO_OP_MOVE_INDEX, 0, 0); - _fastSwitchReveal(battleKey2, false, uint240(1)); - _fastTurn(battleKey2, SWITCH_MOVE_INDEX, 2, uint240(1), 0); + _fastSwitchReveal(battleKey2, false, uint16(1)); + _fastTurn(battleKey2, SWITCH_MOVE_INDEX, 2, uint16(1), 0); _fastTurn(battleKey2, 3, NO_OP_MOVE_INDEX, _packStatBoost(0, 1, uint256(MonStateIndexName.Attack), int32(90)), 0); _fastTurn(battleKey2, 0, NO_OP_MOVE_INDEX, 0, 0); - _fastSwitchReveal(battleKey2, false, uint240(2)); + _fastSwitchReveal(battleKey2, false, uint16(2)); _fastTurn(battleKey2, NO_OP_MOVE_INDEX, 3, 0, _packStatBoost(1, 2, uint256(MonStateIndexName.Attack), int32(90))); _fastTurn(battleKey2, NO_OP_MOVE_INDEX, 0, 0, 0); - _fastSwitchReveal(battleKey2, true, uint240(2)); + _fastSwitchReveal(battleKey2, true, uint16(2)); _fastTurn(battleKey2, 3, 3, _packStatBoost(0, 2, uint256(MonStateIndexName.Attack), int32(90)), _packStatBoost(1, 2, uint256(MonStateIndexName.Attack), int32(90))); _fastTurn(battleKey2, 0, NO_OP_MOVE_INDEX, 0, 0); - _fastSwitchReveal(battleKey2, false, uint240(3)); + _fastSwitchReveal(battleKey2, false, uint16(3)); _fastTurn(battleKey2, 0, NO_OP_MOVE_INDEX, 0, 0); uint256 secondBattleGas = vm.stopSnapshotGas("Fast_Battle2"); @@ -926,19 +886,19 @@ contract FullyOptimizedInlineGasTest is Test, BattleHelper, EIP712 { vm.warp(vm.getBlockTimestamp() + 1); vm.startSnapshotGas("Fast_Battle3"); - _fastTurn(battleKey3, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _fastTurn(battleKey3, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); _fastTurn(battleKey3, 0, 1, 0, 0); - _fastTurn(battleKey3, SWITCH_MOVE_INDEX, 2, uint240(1), _packStatBoost(1, 0, uint256(MonStateIndexName.Attack), int32(90))); + _fastTurn(battleKey3, SWITCH_MOVE_INDEX, 2, uint16(1), _packStatBoost(1, 0, uint256(MonStateIndexName.Attack), int32(90))); _fastTurn(battleKey3, 2, 3, _packStatBoost(0, 1, uint256(MonStateIndexName.Attack), int32(90)), 0); - _fastSwitchReveal(battleKey3, true, uint240(0)); + _fastSwitchReveal(battleKey3, true, uint16(0)); _fastTurn(battleKey3, 2, NO_OP_MOVE_INDEX, _packStatBoost(0, 0, uint256(MonStateIndexName.Attack), int32(90)), 0); _fastTurn(battleKey3, 3, NO_OP_MOVE_INDEX, 0, 0); - _fastSwitchReveal(battleKey3, false, uint240(1)); + _fastSwitchReveal(battleKey3, false, uint16(1)); _fastTurn(battleKey3, NO_OP_MOVE_INDEX, 2, 0, _packStatBoost(1, 1, uint256(MonStateIndexName.Attack), int32(90))); _fastTurn(battleKey3, NO_OP_MOVE_INDEX, 3, 0, 0); - _fastSwitchReveal(battleKey3, true, uint240(2)); + _fastSwitchReveal(battleKey3, true, uint16(2)); _fastTurn(battleKey3, NO_OP_MOVE_INDEX, 3, 0, 0); - _fastSwitchReveal(battleKey3, true, uint240(3)); + _fastSwitchReveal(battleKey3, true, uint16(3)); _fastTurn(battleKey3, NO_OP_MOVE_INDEX, 3, 0, 0); uint256 thirdBattleGas = vm.stopSnapshotGas("Fast_Battle3"); diff --git a/test/InlineMoveParityTest.sol b/test/InlineMoveParityTest.sol index 019395f0..7ffaeaf4 100644 --- a/test/InlineMoveParityTest.sol +++ b/test/InlineMoveParityTest.sol @@ -111,9 +111,9 @@ contract InlineMoveParityTest is Test, BattleHelper { } function _doSwitchTurn(bytes32 battleKey) internal { - bytes32 salt = ""; + uint104 salt = 0; uint256 turnId = engine.getTurnIdForBattleState(battleKey); - bytes32 moveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint240(0))); + bytes32 moveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint16(0))); if (turnId % 2 == 0) { vm.startPrank(ALICE); commitManager.commitMove(battleKey, moveHash); @@ -134,10 +134,10 @@ contract InlineMoveParityTest is Test, BattleHelper { } function _doAttackTurn(bytes32 battleKey, uint8 aliceMove, uint8 bobMove) internal { - bytes32 salt = ""; + uint104 salt = 0; uint256 turnId = engine.getTurnIdForBattleState(battleKey); if (turnId % 2 == 0) { - bytes32 moveHash = keccak256(abi.encodePacked(aliceMove, salt, uint240(0))); + bytes32 moveHash = keccak256(abi.encodePacked(aliceMove, salt, uint16(0))); vm.startPrank(ALICE); commitManager.commitMove(battleKey, moveHash); vm.startPrank(BOB); @@ -145,7 +145,7 @@ contract InlineMoveParityTest is Test, BattleHelper { vm.startPrank(ALICE); commitManager.revealMove(battleKey, aliceMove, salt, 0, true); } else { - bytes32 moveHash = keccak256(abi.encodePacked(bobMove, salt, uint240(0))); + bytes32 moveHash = keccak256(abi.encodePacked(bobMove, salt, uint16(0))); vm.startPrank(BOB); commitManager.commitMove(battleKey, moveHash); vm.startPrank(ALICE); diff --git a/test/InlineValidationTest.sol b/test/InlineValidationTest.sol index 823bd40c..229f9083 100644 --- a/test/InlineValidationTest.sol +++ b/test/InlineValidationTest.sol @@ -125,9 +125,9 @@ contract InlineValidationTest is Test, BattleHelper { bytes32 battleKey = _startBattleWithInlineValidation(); // Both players switch in mon 0 - bytes32 salt = ""; - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint240(0))); - bytes32 p1MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint240(0))); + uint104 salt = 0; + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint16(0))); + bytes32 p1MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint16(0))); vm.startPrank(p0); commitManager.commitMove(battleKey, p0MoveHash); @@ -150,8 +150,8 @@ contract InlineValidationTest is Test, BattleHelper { bytes32 battleKey = _startBattleWithInlineValidation(); // Both players switch in mon 0 - bytes32 salt = ""; - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint240(0))); + uint104 salt = 0; + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint16(0))); vm.startPrank(p0); commitManager.commitMove(battleKey, p0MoveHash); @@ -161,7 +161,7 @@ contract InlineValidationTest is Test, BattleHelper { commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, salt, 0, true); engine.resetCallContext(); // Now use move 0 (attack) - bytes32 p0AttackHash = keccak256(abi.encodePacked(uint8(0), salt, uint240(0))); + bytes32 p0AttackHash = keccak256(abi.encodePacked(uint8(0), salt, uint16(0))); vm.startPrank(p1); commitManager.commitMove(battleKey, p0AttackHash); @@ -180,8 +180,8 @@ contract InlineValidationTest is Test, BattleHelper { bytes32 battleKey = _startBattleWithInlineValidation(); // Both players switch in mon 0 - bytes32 salt = ""; - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint240(0))); + uint104 salt = 0; + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint16(0))); vm.startPrank(p0); commitManager.commitMove(battleKey, p0MoveHash); @@ -192,7 +192,7 @@ contract InlineValidationTest is Test, BattleHelper { engine.resetCallContext(); // P1 commits turn 1 - try to switch to mon 0 again (invalid - already active) // The inline validation should treat this as invalid and fall through - bytes32 p1InvalidSwitchHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint240(0))); + bytes32 p1InvalidSwitchHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint16(0))); vm.startPrank(p1); commitManager.commitMove(battleKey, p1InvalidSwitchHash); @@ -213,10 +213,10 @@ contract InlineValidationTest is Test, BattleHelper { /// @notice Test multiple turns with inline validation function test_inlineValidation_multipleRounds() public { bytes32 battleKey = _startBattleWithInlineValidation(); - bytes32 salt = ""; + uint104 salt = 0; // Turn 0: Both switch in mon 0 - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint240(0))); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint16(0))); vm.startPrank(p0); commitManager.commitMove(battleKey, p0MoveHash); vm.startPrank(p1); @@ -233,7 +233,7 @@ contract InlineValidationTest is Test, BattleHelper { if (bd.playerSwitchForTurnFlag != 2) break; uint256 turnId = engine.getTurnIdForBattleState(battleKey); - bytes32 attackHash = keccak256(abi.encodePacked(uint8(0), salt, uint240(0))); + bytes32 attackHash = keccak256(abi.encodePacked(uint8(0), salt, uint16(0))); if (turnId % 2 == 0) { vm.startPrank(p0); @@ -290,8 +290,8 @@ contract InlineValidationTest is Test, BattleHelper { bytes32 battleKey = _startBattleWithInlineValidation(); // Complete turn 0 switches - bytes32 salt = ""; - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint240(0))); + uint104 salt = 0; + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint16(0))); vm.startPrank(p0); commitManager.commitMove(battleKey, p0MoveHash); vm.startPrank(p1); @@ -321,8 +321,8 @@ contract InlineValidationTest is Test, BattleHelper { bytes32 battleKey = _startBattleWithInlineValidation(); // Complete turn 0 switches - bytes32 salt = ""; - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint240(0))); + uint104 salt = 0; + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint16(0))); vm.startPrank(p0); commitManager.commitMove(battleKey, p0MoveHash); vm.startPrank(p1); @@ -419,8 +419,8 @@ contract InlineValidationTest is Test, BattleHelper { (bytes32 battleKey, DefaultValidator externalValidator) = _startBattleWithExternalValidator(); // Complete turn 0 switches - bytes32 salt = ""; - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint240(0))); + uint104 salt = 0; + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, salt, uint16(0))); vm.startPrank(p0); commitManager.commitMove(battleKey, p0MoveHash); vm.startPrank(p1); @@ -488,7 +488,7 @@ contract InlineValidationTest is Test, BattleHelper { // P0 selects mon 0, CPU will randomly select (mockRNG returns 0, so mon 0) mockRNG.setRNG(0); - cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, "", 0); + cpu.selectMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), 0); // Verify both players switched in assertEq(engine.getActiveMonIndexForBattleState(battleKey)[0], 0, "P0 should have mon 0 active"); @@ -504,7 +504,7 @@ contract InlineValidationTest is Test, BattleHelper { // P0 uses attack, CPU will use attack (mockRNG selects index 1 which is the move) mockRNG.setRNG(1); - cpu.selectMove(battleKey, 0, "", 0); + cpu.selectMove(battleKey, 0, uint104(0), 0); // Battle should have advanced to turn 2 uint256 turnId = engine.getTurnIdForBattleState(battleKey); diff --git a/test/MatchmakerTest.sol b/test/MatchmakerTest.sol index 8678845f..54892be9 100644 --- a/test/MatchmakerTest.sol +++ b/test/MatchmakerTest.sol @@ -393,7 +393,7 @@ contract MatchmakerTest is Test, BattleHelper { assertEq(battleData.p1, BOB); // Check that Alice and Bob can commit/reveal/reveal to switch to mon index 0 - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); } function test_fastBattleSucceedsAndNoSubsequentAccept() public { diff --git a/test/SignedCommitManager.t.sol b/test/SignedCommitManager.t.sol index f9220256..43abefe3 100644 --- a/test/SignedCommitManager.t.sol +++ b/test/SignedCommitManager.t.sol @@ -17,11 +17,11 @@ import {MockRandomnessOracle} from "./mocks/MockRandomnessOracle.sol"; import {TestTeamRegistry} from "./mocks/TestTeamRegistry.sol"; import {DefaultMatchmaker} from "../src/matchmaker/DefaultMatchmaker.sol"; import {BattleHelper} from "./abstract/BattleHelper.sol"; +import {SignedCommitHelper} from "./abstract/SignedCommitHelper.sol"; import {SignedCommitLib} from "../src/commit-manager/SignedCommitLib.sol"; import {TestMoveFactory} from "./mocks/TestMoveFactory.sol"; -import {EIP712} from "../src/lib/EIP712.sol"; -abstract contract SignedCommitManagerTestBase is Test, BattleHelper, EIP712 { +abstract contract SignedCommitManagerTestBase is BattleHelper, SignedCommitHelper { Engine engine; SignedCommitManager signedCommitManager; MockRandomnessOracle mockOracle; @@ -36,11 +36,6 @@ abstract contract SignedCommitManagerTestBase is Test, BattleHelper, EIP712 { address p0; address p1; - // Required by EIP712 inheritance (only used to access _DOMAIN_TYPEHASH) - function _domainNameAndVersion() internal pure override returns (string memory, string memory) { - return ("SignedCommitManager", "1"); - } - function setUp() public virtual { p0 = vm.addr(P0_PK); p1 = vm.addr(P1_PK); @@ -130,96 +125,19 @@ abstract contract SignedCommitManagerTestBase is Test, BattleHelper, EIP712 { return battleKey; } - /// @dev Signs a DualSignedReveal struct for the revealer - /// @param privateKey The revealer's private key - /// @param battleKey The battle identifier - /// @param turnId The current turn ID - /// @param committerMoveHash The committer's move hash (that revealer signs over) - /// @param revealerMoveIndex The revealer's move index - /// @param revealerSalt The revealer's salt - /// @param revealerExtraData The revealer's extra data - function _signDualReveal( - uint256 privateKey, - bytes32 battleKey, - uint64 turnId, - bytes32 committerMoveHash, - uint8 revealerMoveIndex, - bytes32 revealerSalt, - uint240 revealerExtraData - ) internal view returns (bytes memory) { - bytes32 domainSeparator = keccak256( - abi.encode( - _DOMAIN_TYPEHASH, - keccak256("SignedCommitManager"), - keccak256("1"), - block.chainid, - address(signedCommitManager) - ) - ); - - bytes32 structHash = SignedCommitLib.hashDualSignedReveal( - SignedCommitLib.DualSignedReveal({ - battleKey: battleKey, - turnId: turnId, - committerMoveHash: committerMoveHash, - revealerMoveIndex: revealerMoveIndex, - revealerSalt: revealerSalt, - revealerExtraData: revealerExtraData - }) - ); - bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); - return abi.encodePacked(r, s, v); - } - - /// @dev Signs a SignedCommit struct for the committer - /// @param privateKey The committer's private key - /// @param moveHash The committer's move hash - /// @param battleKey The battle identifier - /// @param turnId The current turn ID - function _signCommit( - uint256 privateKey, - bytes32 moveHash, - bytes32 battleKey, - uint64 turnId - ) internal view returns (bytes memory) { - bytes32 domainSeparator = keccak256( - abi.encode( - _DOMAIN_TYPEHASH, - keccak256("SignedCommitManager"), - keccak256("1"), - block.chainid, - address(signedCommitManager) - ) - ); - - bytes32 structHash = SignedCommitLib.hashSignedCommit( - SignedCommitLib.SignedCommit({ - moveHash: moveHash, - battleKey: battleKey, - turnId: turnId - }) - ); - bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); - return abi.encodePacked(r, s, v); - } - /// @dev Completes a turn using the normal commit-reveal flow. /// Turn 0 uses SWITCH_MOVE_INDEX; subsequent turns use NO_OP_MOVE_INDEX. function _completeTurnNormal(bytes32 battleKey, uint256 turnId) internal { - bytes32 salt = bytes32(turnId + 1); + uint104 salt = uint104(turnId + 1); uint8 moveIndex = turnId == 0 ? SWITCH_MOVE_INDEX : NO_OP_MOVE_INDEX; - bytes32 moveHash = keccak256(abi.encodePacked(moveIndex, salt, uint240(0))); + bytes32 moveHash = keccak256(abi.encodePacked(moveIndex, salt, uint16(0))); if (turnId % 2 == 0) { // p0 commits vm.startPrank(p0); signedCommitManager.commitMove(battleKey, moveHash); vm.startPrank(p1); - signedCommitManager.revealMove(battleKey, moveIndex, bytes32(0), 0, false); + signedCommitManager.revealMove(battleKey, moveIndex, uint104(0), 0, false); vm.startPrank(p0); signedCommitManager.revealMove(battleKey, moveIndex, salt, 0, true); } else { @@ -227,7 +145,7 @@ abstract contract SignedCommitManagerTestBase is Test, BattleHelper, EIP712 { vm.startPrank(p1); signedCommitManager.commitMove(battleKey, moveHash); vm.startPrank(p0); - signedCommitManager.revealMove(battleKey, moveIndex, bytes32(0), 0, false); + signedCommitManager.revealMove(battleKey, moveIndex, uint104(0), 0, false); vm.startPrank(p1); signedCommitManager.revealMove(battleKey, moveIndex, salt, 0, true); } @@ -238,44 +156,31 @@ abstract contract SignedCommitManagerTestBase is Test, BattleHelper, EIP712 { /// @dev Completes a turn using the dual-signed flow (1 TX). /// Turn 0 uses SWITCH_MOVE_INDEX; subsequent turns use NO_OP_MOVE_INDEX. function _completeTurnFast(bytes32 battleKey, uint256 turnId) internal { - bytes32 committerSalt = bytes32(turnId + 1); - bytes32 revealerSalt = bytes32(turnId + 2); + uint104 committerSalt = uint104(turnId + 1); + uint104 revealerSalt = uint104(turnId + 2); uint8 moveIndex = turnId == 0 ? SWITCH_MOVE_INDEX : NO_OP_MOVE_INDEX; - bytes32 committerMoveHash = keccak256(abi.encodePacked(moveIndex, committerSalt, uint240(0))); + bytes32 committerMoveHash = keccak256(abi.encodePacked(moveIndex, committerSalt, uint16(0))); - if (turnId % 2 == 0) { - // p0 is committer, p1 is revealer - bytes memory revealerSignature = _signDualReveal( - P1_PK, battleKey, uint64(turnId), committerMoveHash, moveIndex, revealerSalt, 0 - ); - vm.startPrank(p0); - signedCommitManager.executeWithDualSignedMoves( - battleKey, - moveIndex, - committerSalt, - 0, - moveIndex, - revealerSalt, - 0, - revealerSignature - ); - } else { - // p1 is committer, p0 is revealer - bytes memory revealerSignature = _signDualReveal( - P0_PK, battleKey, uint64(turnId), committerMoveHash, moveIndex, revealerSalt, 0 - ); - vm.startPrank(p1); - signedCommitManager.executeWithDualSignedMoves( - battleKey, - moveIndex, - committerSalt, - 0, - moveIndex, - revealerSalt, - 0, - revealerSignature - ); - } + (uint256 committerPk, uint256 revealerPk) = turnId % 2 == 0 ? (P0_PK, P1_PK) : (P1_PK, P0_PK); + bytes memory committerSignature = + _signCommit(address(signedCommitManager), committerPk, committerMoveHash, battleKey, uint64(turnId)); + bytes memory revealerSignature = _signDualReveal(address(signedCommitManager), + revealerPk, battleKey, uint64(turnId), committerMoveHash, moveIndex, revealerSalt, 0 + ); + + // Caller can be anyone; pick committer for parity with old test setup. + vm.startPrank(turnId % 2 == 0 ? p0 : p1); + signedCommitManager.executeWithDualSignedMoves( + battleKey, + moveIndex, + committerSalt, + 0, + moveIndex, + revealerSalt, + 0, + committerSignature, + revealerSignature + ); vm.stopPrank(); engine.resetCallContext(); } @@ -294,12 +199,13 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { uint64 turnId = 0; // p0 creates commitment hash off-chain - bytes32 p0Salt = bytes32(uint256(1)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint240(0))); + uint104 p0Salt = uint104(1); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint16(0))); - // p1 signs their move + p0's hash - bytes32 p1Salt = bytes32(uint256(2)); - bytes memory p1Signature = _signDualReveal( + // p0 signs their commitment, p1 signs their move + p0's hash + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey, turnId); + uint104 p1Salt = uint104(2); + bytes memory p1Signature = _signDualReveal(address(signedCommitManager), P1_PK, battleKey, turnId, p0MoveHash, SWITCH_MOVE_INDEX, p1Salt, 0 ); @@ -313,6 +219,7 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { SWITCH_MOVE_INDEX, p1Salt, 0, + p0CommitSig, p1Signature ); @@ -333,11 +240,12 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { // Turn 1: p1 is committer, p0 is revealer uint64 turnId = 1; - bytes32 p1Salt = bytes32(uint256(2)); - bytes32 p1MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, p1Salt, uint240(0))); + uint104 p1Salt = uint104(2); + bytes32 p1MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, p1Salt, uint16(0))); + bytes memory p1CommitSig = _signCommit(address(signedCommitManager), P1_PK, p1MoveHash, battleKey, turnId); - bytes32 p0Salt = bytes32(uint256(3)); - bytes memory p0Signature = _signDualReveal( + uint104 p0Salt = uint104(3); + bytes memory p0Signature = _signDualReveal(address(signedCommitManager), P0_PK, battleKey, turnId, p1MoveHash, NO_OP_MOVE_INDEX, p0Salt, 0 ); @@ -350,6 +258,7 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { NO_OP_MOVE_INDEX, p0Salt, 0, + p1CommitSig, p0Signature ); @@ -418,7 +327,11 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { function test_revert_invalidSignature() public { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); - bytes32 p0Salt = bytes32(uint256(1)); + uint104 p0Salt = uint104(1); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint16(0))); + + // Valid committer sig, but garbage revealer sig. + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey, 0); bytes memory invalidSignature = abi.encodePacked(bytes32(uint256(1)), bytes32(uint256(2)), uint8(27)); vm.startPrank(p0); @@ -429,8 +342,9 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { p0Salt, 0, SWITCH_MOVE_INDEX, - bytes32(0), + uint104(0), 0, + p0CommitSig, invalidSignature ); } @@ -438,12 +352,13 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { function test_revert_wrongSigner() public { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); - bytes32 p0Salt = bytes32(uint256(1)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint240(0))); + uint104 p0Salt = uint104(1); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint16(0))); - // p0 signs instead of p1 (wrong signer - should be revealer p1) - bytes memory wrongSignature = _signDualReveal( - P0_PK, battleKey, 0, p0MoveHash, SWITCH_MOVE_INDEX, bytes32(0), 0 + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey, 0); + // p0 signs the revealer slot instead of p1 (wrong signer - should be revealer p1) + bytes memory wrongSignature = _signDualReveal(address(signedCommitManager), + P0_PK, battleKey, 0, p0MoveHash, SWITCH_MOVE_INDEX, uint104(0), 0 ); vm.startPrank(p0); @@ -454,8 +369,9 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { p0Salt, 0, SWITCH_MOVE_INDEX, - bytes32(0), + uint104(0), 0, + p0CommitSig, wrongSignature ); } @@ -467,13 +383,14 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { _completeTurnNormal(battleKey, 0); _completeTurnNormal(battleKey, 1); - // On turn 2, p0 is committer again. Try to replay a turn 0 signature. - bytes32 p0Salt = bytes32(uint256(1)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, p0Salt, uint240(0))); + // On turn 2, p0 is committer again. Try to replay turn-0 signatures. + uint104 p0Salt = uint104(1); + bytes32 p0MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, p0Salt, uint16(0))); - // Create signature for turn 0 (not current turn 2) - bytes memory turn0Signature = _signDualReveal( - P1_PK, battleKey, 0, p0MoveHash, NO_OP_MOVE_INDEX, bytes32(0), 0 + // Both signatures bound to turnId=0, replayed at turnId=2 + bytes memory turn0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey, 0); + bytes memory turn0Signature = _signDualReveal(address(signedCommitManager), + P1_PK, battleKey, 0, p0MoveHash, NO_OP_MOVE_INDEX, uint104(0), 0 ); vm.startPrank(p0); @@ -484,8 +401,9 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { p0Salt, 0, NO_OP_MOVE_INDEX, - bytes32(0), + uint104(0), 0, + turn0CommitSig, turn0Signature ); } @@ -493,15 +411,16 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { function test_revert_replayAttack_differentBattle() public { bytes32 battleKey1 = _startBattleWith(address(signedCommitManager)); - bytes32 p0Salt = bytes32(uint256(1)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint240(0))); + uint104 p0Salt = uint104(1); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint16(0))); - // Create signature for battle 1 - bytes memory battle1Signature = _signDualReveal( - P1_PK, battleKey1, 0, p0MoveHash, SWITCH_MOVE_INDEX, bytes32(0), 0 + // Both signatures bound to battle 1 + bytes memory battle1CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey1, 0); + bytes memory battle1Signature = _signDualReveal(address(signedCommitManager), + P1_PK, battleKey1, 0, p0MoveHash, SWITCH_MOVE_INDEX, uint104(0), 0 ); - // Start second battle and try to use battle 1's signature + // Start second battle and try to use battle 1's signatures bytes32 battleKey2 = _startBattleWith(address(signedCommitManager)); vm.startPrank(p0); @@ -512,33 +431,146 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { p0Salt, 0, SWITCH_MOVE_INDEX, - bytes32(0), + uint104(0), 0, + battle1CommitSig, battle1Signature ); } - function test_revert_callerNotCommitter() public { + /// @notice Regression: a revealer alone (without an explicit committer signature) cannot + /// inject a self-chosen committer preimage `P*`. Previously this was blocked only by the + /// `msg.sender == committer` check; now both signatures are mandatory and bind each + /// player independently, so the check holds even under a relayer model. + function test_revert_executeWithDualSigned_unilateralRevealerAttack() public { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); - bytes32 p0Salt = bytes32(uint256(1)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint240(0))); + // Attacker (p1, the revealer for turn 0) picks a preimage P* of their choosing for p0 + uint104 attackerCommitterSalt = uint104(0xdead); + uint16 attackerCommitterExtraData = 0; + uint8 attackerCommitterMoveIndex = SWITCH_MOVE_INDEX; + bytes32 chosenCommitterMoveHash = keccak256( + abi.encodePacked(attackerCommitterMoveIndex, attackerCommitterSalt, attackerCommitterExtraData) + ); - bytes memory p1Signature = _signDualReveal( - P1_PK, battleKey, 0, p0MoveHash, SWITCH_MOVE_INDEX, bytes32(0), 0 + // p1 signs the DualSignedReveal binding themselves to a chosen committer preimage + bytes memory p1Signature = _signDualReveal(address(signedCommitManager), + P1_PK, battleKey, 0, chosenCommitterMoveHash, SWITCH_MOVE_INDEX, uint104(0), 0 ); - // p1 (revealer) tries to call executeWithDualSignedMoves - should fail - vm.startPrank(p1); - vm.expectRevert(SignedCommitManager.CallerNotCommitter.selector); + // Attacker forges a "committer signature" (signed by themselves, P1, over the same hash). + bytes memory forgedCommitterSig = _signCommit(address(signedCommitManager), P1_PK, chosenCommitterMoveHash, battleKey, 0); + + // _startBattleWith leaves an active prank on p0; clear it. + vm.stopPrank(); + + // Submit (from any sender) — committer sig recover will return p1, not p0 → revert. + vm.expectRevert(SignedCommitManager.InvalidSignature.selector); + signedCommitManager.executeWithDualSignedMoves( + battleKey, + attackerCommitterMoveIndex, + attackerCommitterSalt, + attackerCommitterExtraData, + SWITCH_MOVE_INDEX, + uint104(0), + 0, + forgedCommitterSig, + p1Signature + ); + } + + /// @notice Drops the old `msg.sender == committer` check: anyone can submit when both + /// EIP-712 signatures are present and valid (relayer-friendly). + function test_executeWithDualSigned_thirdPartyRelay_succeeds() public { + bytes32 battleKey = _startBattleWith(address(signedCommitManager)); + + uint104 p0Salt = uint104(1); + uint104 p1Salt = uint104(2); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint16(0))); + + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey, 0); + bytes memory p1Signature = _signDualReveal(address(signedCommitManager), + P1_PK, battleKey, 0, p0MoveHash, SWITCH_MOVE_INDEX, p1Salt, 0 + ); + + // _startBattleWith leaves an active prank on p0; clear it before pranking the relayer. + vm.stopPrank(); + + // A random third party (neither p0 nor p1) can submit the bundle. + address relayer = address(0xCAFE); + vm.prank(relayer); + signedCommitManager.executeWithDualSignedMoves( + battleKey, + SWITCH_MOVE_INDEX, + p0Salt, + 0, + SWITCH_MOVE_INDEX, + p1Salt, + 0, + p0CommitSig, + p1Signature + ); + + assertEq(engine.getTurnIdForBattleState(battleKey), 1, "Turn should advance via relayer"); + } + + /// @notice Wrong committer signer (sig recovers to revealer's address, not committer's) reverts. + function test_revert_executeWithDualSigned_wrongCommitterSigner() public { + bytes32 battleKey = _startBattleWith(address(signedCommitManager)); + + uint104 p0Salt = uint104(1); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint16(0))); + + // p1 signs the SignedCommit instead of p0 → recovers to p1, not the committer p0. + bytes memory wrongCommitSig = _signCommit(address(signedCommitManager), P1_PK, p0MoveHash, battleKey, 0); + bytes memory p1Signature = _signDualReveal(address(signedCommitManager), + P1_PK, battleKey, 0, p0MoveHash, SWITCH_MOVE_INDEX, uint104(0), 0 + ); + + vm.startPrank(p0); + vm.expectRevert(SignedCommitManager.InvalidSignature.selector); + signedCommitManager.executeWithDualSignedMoves( + battleKey, + SWITCH_MOVE_INDEX, + p0Salt, + 0, + SWITCH_MOVE_INDEX, + uint104(0), + 0, + wrongCommitSig, + p1Signature + ); + } + + /// @notice Committer signature over a different `moveHash` than the submitted preimage + /// reverts with InvalidSignature (the recovered hash differs from what the engine computes). + function test_revert_executeWithDualSigned_committerSigForWrongHash() public { + bytes32 battleKey = _startBattleWith(address(signedCommitManager)); + + uint104 p0Salt = uint104(1); + bytes32 p0DifferentMoveHash = + keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, p0Salt, uint16(0))); // committer signs over a different move + + bytes memory mismatchedCommitSig = _signCommit(address(signedCommitManager), P0_PK, p0DifferentMoveHash, battleKey, 0); + // Revealer signs the same different hash so the revealer side would have validated + bytes memory p1Signature = _signDualReveal(address(signedCommitManager), + P1_PK, battleKey, 0, p0DifferentMoveHash, SWITCH_MOVE_INDEX, uint104(0), 0 + ); + + // p0 submits with their REAL move data (SWITCH_MOVE_INDEX, p0Salt, 0). Engine recomputes + // committerMoveHash from those fields → does not equal `p0DifferentMoveHash`. Committer sig + // recovery against the recomputed hash returns a non-p0 address → InvalidSignature. + vm.startPrank(p0); + vm.expectRevert(SignedCommitManager.InvalidSignature.selector); signedCommitManager.executeWithDualSignedMoves( battleKey, SWITCH_MOVE_INDEX, p0Salt, 0, SWITCH_MOVE_INDEX, - bytes32(0), + uint104(0), 0, + mismatchedCommitSig, p1Signature ); } @@ -550,11 +582,12 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { function test_revert_battleNotStarted() public { bytes32 fakeBattleKey = bytes32(uint256(123)); - bytes32 p0Salt = bytes32(uint256(1)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint240(0))); + uint104 p0Salt = uint104(1); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint16(0))); - bytes memory p1Signature = _signDualReveal( - P1_PK, fakeBattleKey, 0, p0MoveHash, SWITCH_MOVE_INDEX, bytes32(0), 0 + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, fakeBattleKey, 0); + bytes memory p1Signature = _signDualReveal(address(signedCommitManager), + P1_PK, fakeBattleKey, 0, p0MoveHash, SWITCH_MOVE_INDEX, uint104(0), 0 ); vm.startPrank(p0); @@ -565,8 +598,9 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { p0Salt, 0, SWITCH_MOVE_INDEX, - bytes32(0), + uint104(0), 0, + p0CommitSig, p1Signature ); } @@ -577,14 +611,16 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { // Execute turn 0 with dual-signed flow _completeTurnFast(battleKey, 0); - // After turn 0, we're now on turn 1 where p1 is committer - // Try to replay with a turn 0 signature - fails because: - // 1. Turn has advanced, so signature turnId (0) doesn't match current turnId (1) - bytes32 p1Salt = bytes32(uint256(99)); - bytes32 p1MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, p1Salt, uint240(0))); + // After turn 0, we're now on turn 1 where p1 is committer. + // Try to replay with turn-0 signatures - fails because turnId in sigs (0) doesn't + // match current turnId (1). + uint104 p1Salt = uint104(99); + bytes32 p1MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, p1Salt, uint16(0))); - bytes memory p0Signature = _signDualReveal( - P0_PK, battleKey, 0, p1MoveHash, NO_OP_MOVE_INDEX, bytes32(0), 0 + // Both signatures are bound to turnId=0 (replay attempt) + bytes memory p1CommitSig = _signCommit(address(signedCommitManager), P1_PK, p1MoveHash, battleKey, 0); + bytes memory p0Signature = _signDualReveal(address(signedCommitManager), + P0_PK, battleKey, 0, p1MoveHash, NO_OP_MOVE_INDEX, uint104(0), 0 ); vm.startPrank(p1); @@ -595,8 +631,9 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { p1Salt, 0, NO_OP_MOVE_INDEX, - bytes32(0), + uint104(0), 0, + p1CommitSig, p0Signature ); } @@ -604,11 +641,12 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { function test_revert_replayPrevented_sameBlockAttempt() public { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); - bytes32 p0Salt = bytes32(uint256(1)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint240(0))); + uint104 p0Salt = uint104(1); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint16(0))); - bytes memory p1Signature = _signDualReveal( - P1_PK, battleKey, 0, p0MoveHash, SWITCH_MOVE_INDEX, bytes32(0), 0 + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey, 0); + bytes memory p1Signature = _signDualReveal(address(signedCommitManager), + P1_PK, battleKey, 0, p0MoveHash, SWITCH_MOVE_INDEX, uint104(0), 0 ); vm.startPrank(p0); @@ -618,22 +656,24 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { p0Salt, 0, SWITCH_MOVE_INDEX, - bytes32(0), + uint104(0), 0, + p0CommitSig, p1Signature ); - // After execution, turn advances to 1. On turn 1, p1 is committer, not p0. - // So if p0 tries to call again, it fails with CallerNotCommitter (turn parity changed) - vm.expectRevert(SignedCommitManager.CallerNotCommitter.selector); + // After execution, turn advances to 1. Replaying the same signatures (turnId=0) at + // turnId=1 fails on the committer signature recovery — sig was bound to turn 0. + vm.expectRevert(SignedCommitManager.InvalidSignature.selector); signedCommitManager.executeWithDualSignedMoves( battleKey, SWITCH_MOVE_INDEX, p0Salt, 0, SWITCH_MOVE_INDEX, - bytes32(0), + uint104(0), 0, + p0CommitSig, p1Signature ); } @@ -642,16 +682,20 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); // p0's actual move data - bytes32 p0Salt = bytes32(uint256(1)); + uint104 p0Salt = uint104(1); + bytes32 p0RealMoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint16(0))); - // p1 signs over a DIFFERENT hash than what p0 will submit - bytes32 fakeP0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(uint256(999)), uint240(0))); + // p0 signs the commitment for the REAL move hash (matches what they'll submit) + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0RealMoveHash, battleKey, 0); - bytes memory p1Signature = _signDualReveal( - P1_PK, battleKey, 0, fakeP0MoveHash, SWITCH_MOVE_INDEX, bytes32(0), 0 + // p1 signs over a DIFFERENT hash than what p0 will submit + bytes32 fakeP0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, uint104(999), uint16(0))); + bytes memory p1Signature = _signDualReveal(address(signedCommitManager), + P1_PK, battleKey, 0, fakeP0MoveHash, SWITCH_MOVE_INDEX, uint104(0), 0 ); - // p0 tries to submit with their real move data, but the hash won't match what p1 signed + // p0 tries to submit with their real move data: committer sig validates (matches + // p0RealMoveHash), but revealer sig was over fakeP0MoveHash → revealer recovery fails. vm.startPrank(p0); vm.expectRevert(SignedCommitManager.InvalidSignature.selector); signedCommitManager.executeWithDualSignedMoves( @@ -660,8 +704,9 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { p0Salt, 0, SWITCH_MOVE_INDEX, - bytes32(0), + uint104(0), 0, + p0CommitSig, p1Signature ); } @@ -669,12 +714,13 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { function test_revert_revealerMoveMismatch() public { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); - bytes32 p0Salt = bytes32(uint256(1)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint240(0))); + uint104 p0Salt = uint104(1); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint16(0))); + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey, 0); // p1 signs with SWITCH_MOVE_INDEX - bytes memory p1Signature = _signDualReveal( - P1_PK, battleKey, 0, p0MoveHash, SWITCH_MOVE_INDEX, bytes32(0), 0 + bytes memory p1Signature = _signDualReveal(address(signedCommitManager), + P1_PK, battleKey, 0, p0MoveHash, SWITCH_MOVE_INDEX, uint104(0), 0 ); // p0 tries to submit with different move for p1 (NO_OP instead of SWITCH) @@ -686,8 +732,9 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { p0Salt, 0, NO_OP_MOVE_INDEX, // Different from what p1 signed! - bytes32(0), + uint104(0), 0, + p0CommitSig, p1Signature ); } @@ -700,11 +747,11 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); // Turn 0: p0 is committer, p1 is revealer - bytes32 p0Salt = bytes32(uint256(1)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint240(0))); + uint104 p0Salt = uint104(1); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint16(0))); // p0 signs their commitment - bytes memory p0CommitSig = _signCommit(P0_PK, p0MoveHash, battleKey, 0); + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey, 0); // p1 (revealer) publishes p0's commitment on-chain vm.startPrank(p1); @@ -716,7 +763,7 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { assertEq(storedTurnId, 0, "Turn ID not stored correctly"); // Now p1 can reveal normally - signedCommitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, bytes32(0), 0, false); + signedCommitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), 0, false); // p0 reveals to complete the turn vm.startPrank(p0); @@ -733,11 +780,11 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { _completeTurnNormal(battleKey, 0); // Turn 1: p1 is committer, p0 is revealer - bytes32 p1Salt = bytes32(uint256(2)); - bytes32 p1MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, p1Salt, uint240(0))); + uint104 p1Salt = uint104(2); + bytes32 p1MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, p1Salt, uint16(0))); // p1 signs their commitment - bytes memory p1CommitSig = _signCommit(P1_PK, p1MoveHash, battleKey, 1); + bytes memory p1CommitSig = _signCommit(address(signedCommitManager), P1_PK, p1MoveHash, battleKey, 1); // p0 (revealer) publishes p1's commitment on-chain vm.startPrank(p0); @@ -749,7 +796,7 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { assertEq(storedTurnId, 1, "Turn ID not stored correctly"); // Now p0 can reveal - signedCommitManager.revealMove(battleKey, NO_OP_MOVE_INDEX, bytes32(0), 0, false); + signedCommitManager.revealMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0, false); // p1 reveals to complete the turn vm.startPrank(p1); @@ -761,9 +808,9 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { function test_commitWithSignature_anyoneCanSubmit() public { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); - bytes32 p0Salt = bytes32(uint256(1)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint240(0))); - bytes memory p0CommitSig = _signCommit(P0_PK, p0MoveHash, battleKey, 0); + uint104 p0Salt = uint104(1); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint16(0))); + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey, 0); // Even p0 themselves can submit their own signed commitment // (though this is equivalent to just calling commitMove) @@ -778,10 +825,10 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { function test_commitWithSignature_revert_wrongSigner() public { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(uint256(1)), uint240(0))); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, uint104(1), uint16(0))); // p1 signs instead of p0 (wrong signer) - bytes memory wrongSig = _signCommit(P1_PK, p0MoveHash, battleKey, 0); + bytes memory wrongSig = _signCommit(address(signedCommitManager), P1_PK, p0MoveHash, battleKey, 0); vm.startPrank(p1); vm.expectRevert(SignedCommitManager.InvalidSignature.selector); @@ -791,10 +838,10 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { function test_commitWithSignature_revert_wrongTurn() public { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(uint256(1)), uint240(0))); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, uint104(1), uint16(0))); // p0 signs for turn 1 instead of turn 0 - bytes memory wrongTurnSig = _signCommit(P0_PK, p0MoveHash, battleKey, 1); + bytes memory wrongTurnSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey, 1); vm.startPrank(p1); vm.expectRevert(SignedCommitManager.InvalidSignature.selector); @@ -805,10 +852,10 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { bytes32 battleKey1 = _startBattleWith(address(signedCommitManager)); bytes32 battleKey2 = _startBattleWith(address(signedCommitManager)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(uint256(1)), uint240(0))); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, uint104(1), uint16(0))); // p0 signs for battle 1 - bytes memory battle1Sig = _signCommit(P0_PK, p0MoveHash, battleKey1, 0); + bytes memory battle1Sig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey1, 0); // Try to use on battle 2 vm.startPrank(p1); @@ -819,8 +866,8 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { function test_commitWithSignature_revert_alreadyCommitted() public { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(uint256(1)), uint240(0))); - bytes memory p0CommitSig = _signCommit(P0_PK, p0MoveHash, battleKey, 0); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, uint104(1), uint16(0))); + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey, 0); // First commit succeeds vm.startPrank(p1); @@ -833,8 +880,8 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { function test_commitWithSignature_revert_battleNotStarted() public { bytes32 fakeBattleKey = bytes32(uint256(123)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(uint256(1)), uint240(0))); - bytes memory p0CommitSig = _signCommit(P0_PK, p0MoveHash, fakeBattleKey, 0); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, uint104(1), uint16(0))); + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, fakeBattleKey, 0); vm.startPrank(p1); vm.expectRevert(DefaultCommitManager.BattleNotYetStarted.selector); @@ -844,14 +891,14 @@ contract SignedCommitManagerTest is SignedCommitManagerTestBase { function test_commitWithSignature_afterNormalCommit_reverts() public { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(uint256(1)), uint240(0))); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, uint104(1), uint16(0))); // p0 commits normally vm.startPrank(p0); signedCommitManager.commitMove(battleKey, p0MoveHash); // Now trying to commit with signature should fail - bytes memory p0CommitSig = _signCommit(P0_PK, p0MoveHash, battleKey, 0); + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey, 0); vm.startPrank(p1); vm.expectRevert(DefaultCommitManager.AlreadyCommited.selector); signedCommitManager.commitWithSignature(battleKey, p0MoveHash, p0CommitSig); @@ -873,19 +920,21 @@ contract SignedCommitManagerEngineSafetyTest is SignedCommitManagerTestBase { bytes32 battleKey, uint64 turnId, uint8 committerMoveIndex, - uint240 committerExtraData, + uint16 committerExtraData, uint8 revealerMoveIndex, - uint240 revealerExtraData + uint16 revealerExtraData ) internal { - bytes32 committerSalt = bytes32(uint256(turnId + 1)); - bytes32 revealerSalt = bytes32(uint256(turnId + 2)); + uint104 committerSalt = uint104(turnId + 1); + uint104 revealerSalt = uint104(turnId + 2); bytes32 committerMoveHash = keccak256(abi.encodePacked(committerMoveIndex, committerSalt, committerExtraData)); // Committer is p0 on even turns, p1 on odd turns. - (uint256 revealerPk, address committerAddr) = turnId % 2 == 0 ? (P1_PK, p0) : (P0_PK, p1); + (uint256 committerPk, uint256 revealerPk, address committerAddr) = + turnId % 2 == 0 ? (P0_PK, P1_PK, p0) : (P1_PK, P0_PK, p1); - bytes memory revealerSig = _signDualReveal( + bytes memory committerSig = _signCommit(address(signedCommitManager), committerPk, committerMoveHash, battleKey, turnId); + bytes memory revealerSig = _signDualReveal(address(signedCommitManager), revealerPk, battleKey, turnId, committerMoveHash, revealerMoveIndex, revealerSalt, revealerExtraData ); @@ -898,6 +947,7 @@ contract SignedCommitManagerEngineSafetyTest is SignedCommitManagerTestBase { revealerMoveIndex, revealerSalt, revealerExtraData, + committerSig, revealerSig ); vm.stopPrank(); @@ -911,7 +961,7 @@ contract SignedCommitManagerEngineSafetyTest is SignedCommitManagerTestBase { // p0 sends a valid switch to mon 1, p1 sends NO_OP (would have been rejected at reveal in // the old flow). Engine must force p1 to switch-to-mon-0. - _executeDualSigned(battleKey, 0, SWITCH_MOVE_INDEX, uint240(1), NO_OP_MOVE_INDEX, 0); + _executeDualSigned(battleKey, 0, SWITCH_MOVE_INDEX, uint16(1), NO_OP_MOVE_INDEX, 0); assertEq(engine.getTurnIdForBattleState(battleKey), 1, "turn should advance"); uint256[] memory active = engine.getActiveMonIndexForBattleState(battleKey); @@ -971,7 +1021,7 @@ contract SignedCommitManagerEngineSafetyTest is SignedCommitManagerTestBase { // Turn 1: p1 commits SWITCH with an out-of-bounds target (team size is 2, submit 99). // p0 NO_OPs. p1's active mon should stay at 0 (switch is a no-op), not wrap to 99 & 0xFF. - _executeDualSigned(battleKey, 1, SWITCH_MOVE_INDEX, uint240(99), NO_OP_MOVE_INDEX, 0); + _executeDualSigned(battleKey, 1, SWITCH_MOVE_INDEX, uint16(99), NO_OP_MOVE_INDEX, 0); assertEq(engine.getTurnIdForBattleState(battleKey), 2, "turn should advance"); uint256[] memory active = engine.getActiveMonIndexForBattleState(battleKey); @@ -983,7 +1033,7 @@ contract SignedCommitManagerEngineSafetyTest is SignedCommitManagerTestBase { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); // Turn 0: p0 switches to mon 1, p1 to mon 0. - _executeDualSigned(battleKey, 0, SWITCH_MOVE_INDEX, uint240(1), SWITCH_MOVE_INDEX, 0); + _executeDualSigned(battleKey, 0, SWITCH_MOVE_INDEX, uint16(1), SWITCH_MOVE_INDEX, 0); assertEq(engine.getActiveMonIndexForBattleState(battleKey)[0], 1); // Turn 1: p1 is committer. p1 tries to switch to their own active mon (0). p0 NO_OP. diff --git a/test/SignedCommitManagerGasBenchmark.t.sol b/test/SignedCommitManagerGasBenchmark.t.sol index 811bbc60..06a732c3 100644 --- a/test/SignedCommitManagerGasBenchmark.t.sol +++ b/test/SignedCommitManagerGasBenchmark.t.sol @@ -26,7 +26,7 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { function test_gasBenchmark_normalFlow_cold() public { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(uint256(1)), uint240(0))); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, uint104(1), uint16(0))); vm.startPrank(p0); uint256 gasBefore = gasleft(); @@ -35,12 +35,12 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { vm.startPrank(p1); gasBefore = gasleft(); - signedCommitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, bytes32(0), 0, false); + signedCommitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), 0, false); gasUsed_normalFlow_cold_reveal1 = gasBefore - gasleft(); vm.startPrank(p0); gasBefore = gasleft(); - signedCommitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, bytes32(uint256(1)), 0, true); + signedCommitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, uint104(1), 0, true); gasUsed_normalFlow_cold_reveal2 = gasBefore - gasleft(); emit log_named_uint("Normal Flow (Cold) - Commit (Alice)", gasUsed_normalFlow_cold_commit); @@ -55,12 +55,13 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { bytes32 battleKey = _startBattleWith(address(signedCommitManager)); // Prepare move data - bytes32 p0Salt = bytes32(uint256(1)); - bytes32 p1Salt = bytes32(uint256(2)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint240(0))); + uint104 p0Salt = uint104(1); + uint104 p1Salt = uint104(2); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint16(0))); - // p1 (revealer) signs their move + p0's hash - bytes memory p1Signature = _signDualReveal( + // Both players sign off-chain + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey, 0); + bytes memory p1Signature = _signDualReveal(address(signedCommitManager), P1_PK, battleKey, 0, p0MoveHash, SWITCH_MOVE_INDEX, p1Salt, 0 ); @@ -75,6 +76,7 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { SWITCH_MOVE_INDEX, p1Salt, 0, + p0CommitSig, p1Signature ); gasUsed_dualSignedFlow_cold = gasBefore - gasleft(); @@ -90,7 +92,7 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { _completeTurnNormal(battleKey, 1); // Turn 2 (warm storage - p0 commits again) - bytes32 p0MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, bytes32(uint256(100)), uint240(0))); + bytes32 p0MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, uint104(100), uint16(0))); vm.startPrank(p0); uint256 gasBefore = gasleft(); @@ -99,12 +101,12 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { vm.startPrank(p1); gasBefore = gasleft(); - signedCommitManager.revealMove(battleKey, NO_OP_MOVE_INDEX, bytes32(0), 0, false); + signedCommitManager.revealMove(battleKey, NO_OP_MOVE_INDEX, uint104(0), 0, false); gasUsed_normalFlow_warm_reveal1 = gasBefore - gasleft(); vm.startPrank(p0); gasBefore = gasleft(); - signedCommitManager.revealMove(battleKey, NO_OP_MOVE_INDEX, bytes32(uint256(100)), 0, true); + signedCommitManager.revealMove(battleKey, NO_OP_MOVE_INDEX, uint104(100), 0, true); gasUsed_normalFlow_warm_reveal2 = gasBefore - gasleft(); emit log_named_uint("Normal Flow (Warm) - Commit (Alice)", gasUsed_normalFlow_warm_commit); @@ -122,11 +124,12 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { _completeTurnNormal(battleKey, 1); // Turn 2 with dual-signed flow (warm storage) - bytes32 p0Salt = bytes32(uint256(100)); - bytes32 p1Salt = bytes32(uint256(101)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, p0Salt, uint240(0))); + uint104 p0Salt = uint104(100); + uint104 p1Salt = uint104(101); + bytes32 p0MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, p0Salt, uint16(0))); - bytes memory p1Signature = _signDualReveal( + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey, 2); + bytes memory p1Signature = _signDualReveal(address(signedCommitManager), P1_PK, battleKey, 2, p0MoveHash, NO_OP_MOVE_INDEX, p1Salt, 0 ); @@ -140,6 +143,7 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { NO_OP_MOVE_INDEX, p1Salt, 0, + p0CommitSig, p1Signature ); gasUsed_dualSignedFlow_warm = gasBefore - gasleft(); @@ -156,7 +160,7 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { // Normal flow cold (3 TXs) { - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, bytes32(uint256(1)), uint240(0))); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, uint104(1), uint16(0))); vm.startPrank(p0); uint256 gasBefore = gasleft(); @@ -165,12 +169,12 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { vm.startPrank(p1); gasBefore = gasleft(); - signedCommitManager.revealMove(battleKey1, SWITCH_MOVE_INDEX, bytes32(0), 0, false); + signedCommitManager.revealMove(battleKey1, SWITCH_MOVE_INDEX, uint104(0), 0, false); gasUsed_normalFlow_cold_reveal1 = gasBefore - gasleft(); vm.startPrank(p0); gasBefore = gasleft(); - signedCommitManager.revealMove(battleKey1, SWITCH_MOVE_INDEX, bytes32(uint256(1)), 0, true); + signedCommitManager.revealMove(battleKey1, SWITCH_MOVE_INDEX, uint104(1), 0, true); gasUsed_normalFlow_cold_reveal2 = gasBefore - gasleft(); } @@ -178,11 +182,12 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { // Reset transient first so a stale execute from battleKey1 above doesn't pollute battleKey2's measurement. engine.resetCallContext(); { - bytes32 p0Salt = bytes32(uint256(1)); - bytes32 p1Salt = bytes32(uint256(2)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint240(0))); + uint104 p0Salt = uint104(1); + uint104 p1Salt = uint104(2); + bytes32 p0MoveHash = keccak256(abi.encodePacked(SWITCH_MOVE_INDEX, p0Salt, uint16(0))); - bytes memory p1Signature = _signDualReveal( + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey2, 0); + bytes memory p1Signature = _signDualReveal(address(signedCommitManager), P1_PK, battleKey2, 0, p0MoveHash, SWITCH_MOVE_INDEX, p1Salt, 0 ); @@ -196,6 +201,7 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { SWITCH_MOVE_INDEX, p1Salt, 0, + p0CommitSig, p1Signature ); gasUsed_dualSignedFlow_cold = gasBefore - gasleft(); @@ -210,7 +216,7 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { // Normal flow warm (turn 2) { - bytes32 p0MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, bytes32(uint256(100)), uint240(0))); + bytes32 p0MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, uint104(100), uint16(0))); vm.startPrank(p0); uint256 gasBefore = gasleft(); @@ -219,22 +225,23 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { vm.startPrank(p1); gasBefore = gasleft(); - signedCommitManager.revealMove(battleKey1, NO_OP_MOVE_INDEX, bytes32(0), 0, false); + signedCommitManager.revealMove(battleKey1, NO_OP_MOVE_INDEX, uint104(0), 0, false); gasUsed_normalFlow_warm_reveal1 = gasBefore - gasleft(); vm.startPrank(p0); gasBefore = gasleft(); - signedCommitManager.revealMove(battleKey1, NO_OP_MOVE_INDEX, bytes32(uint256(100)), 0, true); + signedCommitManager.revealMove(battleKey1, NO_OP_MOVE_INDEX, uint104(100), 0, true); gasUsed_normalFlow_warm_reveal2 = gasBefore - gasleft(); } // Dual-signed flow warm (turn 2) { - bytes32 p0Salt = bytes32(uint256(100)); - bytes32 p1Salt = bytes32(uint256(101)); - bytes32 p0MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, p0Salt, uint240(0))); + uint104 p0Salt = uint104(100); + uint104 p1Salt = uint104(101); + bytes32 p0MoveHash = keccak256(abi.encodePacked(NO_OP_MOVE_INDEX, p0Salt, uint16(0))); - bytes memory p1Signature = _signDualReveal( + bytes memory p0CommitSig = _signCommit(address(signedCommitManager), P0_PK, p0MoveHash, battleKey2, 2); + bytes memory p1Signature = _signDualReveal(address(signedCommitManager), P1_PK, battleKey2, 2, p0MoveHash, NO_OP_MOVE_INDEX, p1Salt, 0 ); @@ -248,6 +255,7 @@ contract SignedCommitManagerGasBenchmarkTest is SignedCommitManagerTestBase { NO_OP_MOVE_INDEX, p1Salt, 0, + p0CommitSig, p1Signature ); gasUsed_dualSignedFlow_warm = gasBefore - gasleft(); diff --git a/test/StandardAttackPvPGasTest.sol b/test/StandardAttackPvPGasTest.sol index 27fda2a1..a0d2e1b0 100644 --- a/test/StandardAttackPvPGasTest.sol +++ b/test/StandardAttackPvPGasTest.sol @@ -14,7 +14,7 @@ import {SignedMatchmaker} from "../src/matchmaker/SignedMatchmaker.sol"; import {BattleOfferLib} from "../src/matchmaker/BattleOfferLib.sol"; import {StandardAttackFactory} from "../src/moves/StandardAttackFactory.sol"; import {ATTACK_PARAMS} from "../src/moves/StandardAttackStructs.sol"; -import {EIP712} from "../src/lib/EIP712.sol"; +import {SignedCommitHelper} from "./abstract/SignedCommitHelper.sol"; import {IEngine} from "../src/IEngine.sol"; import {IEngineHook} from "../src/IEngineHook.sol"; @@ -38,7 +38,7 @@ import {TestTeamRegistry} from "./mocks/TestTeamRegistry.sol"; /// Existing PvP benchmarks (FullyOptimizedInlineGasTest) use CustomAttack / /// EffectAttack / StatBoostsMove — none of which extend StandardAttack — so the /// StandardAttack hot path doesn't show up there. -contract StandardAttackPvPGasTest is Test, EIP712 { +contract StandardAttackPvPGasTest is SignedCommitHelper { uint256 constant MONS_PER_TEAM = 4; uint256 constant MOVES_PER_MON = 4; @@ -55,10 +55,6 @@ contract StandardAttackPvPGasTest is Test, EIP712 { TestTeamRegistry defaultRegistry; StandardAttackFactory attackFactory; - function _domainNameAndVersion() internal pure override returns (string memory, string memory) { - return ("SignedCommitManager", "1"); - } - function setUp() public { p0 = vm.addr(P0_PK); p1 = vm.addr(P1_PK); @@ -116,54 +112,22 @@ contract StandardAttackPvPGasTest is Test, EIP712 { return battleKey; } - function _signDualReveal( - uint256 privateKey, - bytes32 battleKey, - uint64 turnId, - bytes32 committerMoveHash, - uint8 revealerMoveIndex, - bytes32 revealerSalt, - uint240 revealerExtraData - ) internal view returns (bytes memory) { - bytes32 domainSeparator = keccak256( - abi.encode( - _DOMAIN_TYPEHASH, - keccak256("SignedCommitManager"), - keccak256("1"), - block.chainid, - address(signedCommitManager) - ) - ); - bytes32 structHash = SignedCommitLib.hashDualSignedReveal( - SignedCommitLib.DualSignedReveal({ - battleKey: battleKey, - turnId: turnId, - committerMoveHash: committerMoveHash, - revealerMoveIndex: revealerMoveIndex, - revealerSalt: revealerSalt, - revealerExtraData: revealerExtraData - }) - ); - bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); - return abi.encodePacked(r, s, v); - } - function _fastTurn( bytes32 battleKey, uint8 p0MoveIndex, uint8 p1MoveIndex, - uint240 p0ExtraData, - uint240 p1ExtraData + uint16 p0ExtraData, + uint16 p1ExtraData ) internal { uint64 turnId = uint64(engine.getTurnIdForBattleState(battleKey)); - bytes32 committerSalt = keccak256(abi.encode("committer", battleKey, turnId)); - bytes32 revealerSalt = keccak256(abi.encode("revealer", battleKey, turnId)); + uint104 committerSalt = uint104(uint256(keccak256(abi.encode("committer", battleKey, turnId)))); + uint104 revealerSalt = uint104(uint256(keccak256(abi.encode("revealer", battleKey, turnId)))); uint8 committerMoveIndex; - uint240 committerExtraData; + uint16 committerExtraData; uint8 revealerMoveIndex; - uint240 revealerExtraData; + uint16 revealerExtraData; + uint256 committerPk; uint256 revealerPk; address committer; @@ -172,6 +136,7 @@ contract StandardAttackPvPGasTest is Test, EIP712 { committerExtraData = p0ExtraData; revealerMoveIndex = p1MoveIndex; revealerExtraData = p1ExtraData; + committerPk = P0_PK; revealerPk = P1_PK; committer = p0; } else { @@ -179,14 +144,17 @@ contract StandardAttackPvPGasTest is Test, EIP712 { committerExtraData = p1ExtraData; revealerMoveIndex = p0MoveIndex; revealerExtraData = p0ExtraData; + committerPk = P1_PK; revealerPk = P0_PK; committer = p1; } bytes32 committerMoveHash = keccak256(abi.encodePacked(committerMoveIndex, committerSalt, committerExtraData)); + address mgr = address(signedCommitManager); + bytes memory committerSig = _signCommit(mgr, committerPk, committerMoveHash, battleKey, turnId); bytes memory revealerSig = _signDualReveal( - revealerPk, battleKey, turnId, committerMoveHash, revealerMoveIndex, revealerSalt, revealerExtraData + mgr, revealerPk, battleKey, turnId, committerMoveHash, revealerMoveIndex, revealerSalt, revealerExtraData ); vm.prank(committer); @@ -198,6 +166,7 @@ contract StandardAttackPvPGasTest is Test, EIP712 { revealerMoveIndex, revealerSalt, revealerExtraData, + committerSig, revealerSig ); engine.resetCallContext(); @@ -280,7 +249,7 @@ contract StandardAttackPvPGasTest is Test, EIP712 { // Turn 0: lead-in switch. vm.startSnapshotGas("Turn0_Lead"); - _fastTurn(battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _fastTurn(battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); uint256 turn0 = vm.stopSnapshotGas("Turn0_Lead"); // Turns 1-4: pure damage trades. Both players use move 0 / move 1 alternately. diff --git a/test/abstract/BattleHelper.sol b/test/abstract/BattleHelper.sol index 45caac16..c32d6a3d 100644 --- a/test/abstract/BattleHelper.sol +++ b/test/abstract/BattleHelper.sol @@ -25,10 +25,10 @@ abstract contract BattleHelper is Test { bytes32 battleKey, uint8 aliceMoveIndex, uint8 bobMoveIndex, - uint240 aliceExtraData, - uint240 bobExtraData + uint16 aliceExtraData, + uint16 bobExtraData ) internal { - bytes32 salt = ""; + uint104 salt = 0; bytes32 aliceMoveHash = keccak256(abi.encodePacked(aliceMoveIndex, salt, aliceExtraData)); bytes32 bobMoveHash = keccak256(abi.encodePacked(bobMoveIndex, salt, bobExtraData)); // Decide which player commits @@ -64,8 +64,8 @@ abstract contract BattleHelper is Test { DefaultCommitManager commitManager, bytes32 battleKey, uint8 moveIndex, - bytes32 salt, - uint240 extraData + uint104 salt, + uint16 extraData ) internal { commitManager.revealMove(battleKey, moveIndex, salt, extraData, true); vm.stopPrank(); @@ -170,4 +170,19 @@ abstract contract BattleHelper is Test { ability: 0 }); } + + /// @dev Layout used by `test/mocks/StatBoostsMove.sol`: + /// `[boostAmount:8 (signed) | statIndex:4 | monIndex:3 | playerIndex:1]` + function _packStatBoost(uint256 playerIndex, uint256 monIndex, uint256 statIndex, int32 boostAmount) + internal + pure + returns (uint16) + { + return uint16( + (playerIndex & 0x1) + | ((monIndex & 0x7) << 1) + | ((statIndex & 0xF) << 4) + | ((uint256(uint8(int8(boostAmount))) & 0xFF) << 8) + ); + } } diff --git a/test/abstract/SignedCommitHelper.sol b/test/abstract/SignedCommitHelper.sol new file mode 100644 index 00000000..5193f7c5 --- /dev/null +++ b/test/abstract/SignedCommitHelper.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: AGPL-3.0 +pragma solidity ^0.8.0; + +import {SignedCommitLib} from "../../src/commit-manager/SignedCommitLib.sol"; + +import {Test} from "forge-std/Test.sol"; + +/// @notice EIP-712 signing helpers for `SignedCommitManager` tests. +/// @dev Standalone (not inheriting `EIP712`) — replicates `_DOMAIN_TYPEHASH` to avoid pulling +/// the production base into test contracts. The verifying-contract address is taken as a +/// parameter so a single helper instance can sign for multiple managers. +abstract contract SignedCommitHelper is Test { + /// `keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")` + bytes32 internal constant _SIGNED_COMMIT_DOMAIN_TYPEHASH = + 0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f; + + function _signedCommitDomainSeparator(address verifyingContract) internal view returns (bytes32) { + return keccak256( + abi.encode( + _SIGNED_COMMIT_DOMAIN_TYPEHASH, + keccak256("SignedCommitManager"), + keccak256("1"), + block.chainid, + verifyingContract + ) + ); + } + + function _signCommit( + address signedCommitManagerAddr, + uint256 privateKey, + bytes32 moveHash, + bytes32 battleKey, + uint64 turnId + ) internal view returns (bytes memory) { + bytes32 structHash = SignedCommitLib.hashSignedCommit( + SignedCommitLib.SignedCommit({moveHash: moveHash, battleKey: battleKey, turnId: turnId}) + ); + bytes32 digest = keccak256( + abi.encodePacked("\x19\x01", _signedCommitDomainSeparator(signedCommitManagerAddr), structHash) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); + return abi.encodePacked(r, s, v); + } + + function _signDualReveal( + address signedCommitManagerAddr, + uint256 privateKey, + bytes32 battleKey, + uint64 turnId, + bytes32 committerMoveHash, + uint8 revealerMoveIndex, + uint104 revealerSalt, + uint16 revealerExtraData + ) internal view returns (bytes memory) { + bytes32 structHash = SignedCommitLib.hashDualSignedReveal( + SignedCommitLib.DualSignedReveal({ + battleKey: battleKey, + turnId: turnId, + committerMoveHash: committerMoveHash, + revealerMoveIndex: revealerMoveIndex, + revealerSalt: revealerSalt, + revealerExtraData: revealerExtraData + }) + ); + bytes32 digest = keccak256( + abi.encodePacked("\x19\x01", _signedCommitDomainSeparator(signedCommitManagerAddr), structHash) + ); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); + return abi.encodePacked(r, s, v); + } +} diff --git a/test/effects/EffectTest.sol b/test/effects/EffectTest.sol index 95349ff6..86b657b5 100644 --- a/test/effects/EffectTest.sol +++ b/test/effects/EffectTest.sol @@ -146,7 +146,7 @@ contract EffectTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice and Bob both select attacks, both of them are move index 0 (do frostbite damage) @@ -237,11 +237,11 @@ contract EffectTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice switches to mon index 1, Bob induces frostbite - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), 0); // Check that Alice's new mon at index 0 has taken damage assertEq(engine.getMonStateForBattle(battleKey, 0, 1, MonStateIndexName.Hp), -1); @@ -301,7 +301,7 @@ contract EffectTest is Test, BattleHelper { */ bytes32 battleKey = _startBattle(validatorToUse, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, 0, 0, 0); assertEq(engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Stamina), -1); assertEq(engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina), 0); @@ -316,7 +316,7 @@ contract EffectTest is Test, BattleHelper { assertEq(engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Stamina), -1); // Alice is asleep, Bob does nothing, Alice switches to mon index 1, should be successful mockOracle.setRNG(1); - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(1), 0); assertEq(engine.getActiveMonIndexForBattleState(battleKey)[0], 1); } @@ -392,7 +392,7 @@ contract EffectTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice and Bob both select attacks, both of them are move index 0 (inflict panic) @@ -477,7 +477,7 @@ contract EffectTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice and Bob both select attacks, both of them are move index 0 (apply burn status) @@ -628,7 +628,7 @@ contract EffectTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice and Bob both select attacks, both of them are move index 0 (apply zap status) @@ -640,14 +640,14 @@ contract EffectTest is Test, BattleHelper { assertEq(engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.Stamina), -1); // Alice uses Zap, Bob switches to mon index 1 - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, SWITCH_MOVE_INDEX, 0, uint240(1)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, SWITCH_MOVE_INDEX, 0, uint16(1)); // The move should outspeed the swap, so the swap doesn't happen // So Bob's active mon index should still be 0 assertEq(engine.getActiveMonIndexForBattleState(battleKey)[1], 0); // Alice uses slower Zap, Bob switches to mon index 1 - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 1, SWITCH_MOVE_INDEX, 0, uint240(1)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 1, SWITCH_MOVE_INDEX, 0, uint16(1)); // Bob's active mon index should be 1 (swap goes before getting Zapped) assertEq(engine.getActiveMonIndexForBattleState(battleKey)[1], 1); @@ -657,7 +657,7 @@ contract EffectTest is Test, BattleHelper { assertEq(zapEffects.length, 1); // Alice does nothing, Bob attempts to switch to mon index 1, which should succeed because Zap allows switches - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint240(0)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint16(0)); // Check that Bob's active mon index is now 0, and the effect is still there assertEq(engine.getActiveMonIndexForBattleState(battleKey)[1], 0); @@ -665,7 +665,7 @@ contract EffectTest is Test, BattleHelper { assertEq(zapEffectsAfter.length, 1); // Bob switches back to mon index 1, Alice does nothing - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint240(1)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint16(1)); // Bob tries to make a move, Alice does nothing, Zap should skip his turn _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 0, 0, 0); @@ -728,7 +728,7 @@ contract EffectTest is Test, BattleHelper { // First move of the game has to be selecting their mons (both index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses NoDamage, Bob does as well @@ -803,7 +803,7 @@ contract EffectTest is Test, BattleHelper { // First move: both switch in their mons _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Verify Bob's mon has the heal effect applied (from ability on switch in) diff --git a/test/effects/StatBoosts.t.sol b/test/effects/StatBoosts.t.sol index cb276a67..bd0de342 100644 --- a/test/effects/StatBoosts.t.sol +++ b/test/effects/StatBoosts.t.sol @@ -38,11 +38,6 @@ contract StatBoostsTest is Test, BattleHelper { StatBoostsMove statBoostMove; DefaultMatchmaker matchmaker; - // Helper to pack StatBoostsMove extraData: lower 60 bits = playerIndex, next 60 bits = monIndex, next 60 bits = statIndex, upper 60 bits = boostAmount - function _packStatBoost(uint256 playerIndex, uint256 monIndex, uint256 statIndex, int32 boostAmount) internal pure returns (uint240) { - return uint240(playerIndex | (monIndex << 60) | (statIndex << 120) | (uint256(uint32(boostAmount)) << 180)); - } - function setUp() public { typeCalc = new TestTypeCalculator(); mockOracle = new MockRandomnessOracle(); @@ -112,7 +107,7 @@ contract StatBoostsTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // We'll test Attack stat in detail @@ -193,7 +188,7 @@ contract StatBoostsTest is Test, BattleHelper { battleKey, SWITCH_MOVE_INDEX, // Alice switches NO_OP_MOVE_INDEX, // Bob does nothing - uint240(1), // Alice switches to mon 1 + uint16(1), // Alice switches to mon 1 0 // Bob does nothing ); @@ -215,7 +210,7 @@ contract StatBoostsTest is Test, BattleHelper { battleKey, SWITCH_MOVE_INDEX, // Alice switches NO_OP_MOVE_INDEX, // Bob does nothing - uint240(0), // Alice switches back to mon 0 + uint16(0), // Alice switches back to mon 0 0 // Bob does nothing ); @@ -286,7 +281,7 @@ contract StatBoostsTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Test all stats @@ -335,7 +330,7 @@ contract StatBoostsTest is Test, BattleHelper { battleKey, SWITCH_MOVE_INDEX, // Alice switches NO_OP_MOVE_INDEX, // Bob does nothing - uint240(1), // Alice switches to mon 1 + uint16(1), // Alice switches to mon 1 0 // Bob does nothing ); @@ -389,7 +384,7 @@ contract StatBoostsTest is Test, BattleHelper { // Both players select their first mon (index 0) bytes32 battleKey = _startBattle(validatorToUse, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses stat boost move to boost her mon's special atk 50%, Bob does nothing @@ -429,7 +424,7 @@ contract StatBoostsTest is Test, BattleHelper { battleKey, SWITCH_MOVE_INDEX, // Alice switches NO_OP_MOVE_INDEX, // Bob does nothing - uint240(1), // Alice switches to mon 1 + uint16(1), // Alice switches to mon 1 0 // Bob does nothing ); diff --git a/test/mocks/CustomAttack.sol b/test/mocks/CustomAttack.sol index 0f5913a5..9c4a12c0 100644 --- a/test/mocks/CustomAttack.sol +++ b/test/mocks/CustomAttack.sol @@ -55,7 +55,7 @@ contract CustomAttack is IMoveSet { uint256 attackerPlayerIndex, uint256 attackerMonIndex, uint256 defenderMonIndex, - uint240 extraData, + uint16 extraData, uint256 rng ) external { _standardAttack.move(engine, battleKey, attackerPlayerIndex, attackerMonIndex, defenderMonIndex, extraData, rng); @@ -73,7 +73,7 @@ contract CustomAttack is IMoveSet { return _standardAttack.moveType(engine, battleKey); } - function isValidTarget(IEngine engine, bytes32 battleKey, uint240 extraData) external view returns (bool) { + function isValidTarget(IEngine engine, bytes32 battleKey, uint16 extraData) external view returns (bool) { return _standardAttack.isValidTarget(engine, battleKey, extraData); } diff --git a/test/mocks/EditEffectAttack.sol b/test/mocks/EditEffectAttack.sol index ebcffea0..670b8601 100644 --- a/test/mocks/EditEffectAttack.sol +++ b/test/mocks/EditEffectAttack.sol @@ -13,11 +13,12 @@ contract EditEffectAttack is IMoveSet { return "Edit Effect Attack"; } - function move(IEngine engine, bytes32, uint256, uint256, uint256, uint240 extraData, uint256) external { - // Unpack extraData: lower 80 bits = targetIndex, next 80 bits = monIndex, upper 80 bits = effectIndex - uint256 targetIndex = uint256(extraData) & ((1 << 80) - 1); - uint256 monIndex = (uint256(extraData) >> 80) & ((1 << 80) - 1); - uint256 effectIndex = (uint256(extraData) >> 160) & ((1 << 80) - 1); + function move(IEngine engine, bytes32, uint256, uint256, uint256, uint16 extraData, uint256) external { + // Unpack extraData (16 bits): bits 0..1 = targetIndex (0=p0, 1=p1, 2=global), + // bits 2..5 = monIndex (max 15), bits 6..15 = effectIndex (max 1023). + uint256 targetIndex = uint256(extraData) & 0x3; + uint256 monIndex = (uint256(extraData) >> 2) & 0xF; + uint256 effectIndex = (uint256(extraData) >> 6) & 0x3FF; engine.editEffect(targetIndex, monIndex, effectIndex, bytes32(uint256(69))); } @@ -33,7 +34,7 @@ contract EditEffectAttack is IMoveSet { return Type.Fire; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/test/mocks/EffectAttack.sol b/test/mocks/EffectAttack.sol index fe05a28d..dcd68cd5 100644 --- a/test/mocks/EffectAttack.sol +++ b/test/mocks/EffectAttack.sol @@ -33,7 +33,7 @@ contract EffectAttack is IMoveSet { return "Effect Attack"; } - function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, uint240, uint256) external { + function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, uint16, uint256) external { uint256 targetIndex = (attackerPlayerIndex + 1) % 2; engine.addEffect(targetIndex, defenderMonIndex, EFFECT, bytes32(0)); } @@ -50,7 +50,7 @@ contract EffectAttack is IMoveSet { return TYPE; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/test/mocks/ForceSwitchMove.sol b/test/mocks/ForceSwitchMove.sol index 6b83b8fb..d7966f40 100644 --- a/test/mocks/ForceSwitchMove.sol +++ b/test/mocks/ForceSwitchMove.sol @@ -30,10 +30,10 @@ contract ForceSwitchMove is IMoveSet { return "Force Switch"; } - function move(IEngine engine, bytes32, uint256, uint256, uint256, uint240 extraData, uint256) external { - // Decode data as packed (playerIndex in lower 120 bits, monToSwitchIndex in upper 120 bits) - uint256 playerIndex = uint256(extraData) & ((1 << 120) - 1); - uint256 monToSwitchIndex = uint256(extraData) >> 120; + function move(IEngine engine, bytes32, uint256, uint256, uint256, uint16 extraData, uint256) external { + // Pack: bit 0 = playerIndex (0 or 1), bits 1..15 = monToSwitchIndex + uint256 playerIndex = uint256(extraData) & 0x1; + uint256 monToSwitchIndex = uint256(extraData) >> 1; // Use the new switchActiveMon function engine.switchActiveMon(playerIndex, monToSwitchIndex); @@ -51,7 +51,7 @@ contract ForceSwitchMove is IMoveSet { return TYPE; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/test/mocks/GlobalEffectAttack.sol b/test/mocks/GlobalEffectAttack.sol index f5be9b13..b5bcb375 100644 --- a/test/mocks/GlobalEffectAttack.sol +++ b/test/mocks/GlobalEffectAttack.sol @@ -33,7 +33,7 @@ contract GlobalEffectAttack is IMoveSet { return "Effect Attack"; } - function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256, uint240, uint256) external { + function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256, uint16, uint256) external { engine.addEffect(2, 0, EFFECT, bytes32(attackerPlayerIndex)); } @@ -49,7 +49,7 @@ contract GlobalEffectAttack is IMoveSet { return TYPE; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/test/mocks/InvalidMove.sol b/test/mocks/InvalidMove.sol index 3cee3926..dadb8746 100644 --- a/test/mocks/InvalidMove.sol +++ b/test/mocks/InvalidMove.sol @@ -15,7 +15,7 @@ contract InvalidMove is IMoveSet { return "Effect Attack"; } - function move(IEngine, bytes32, uint256, uint256, uint256, uint240, uint256) external pure { + function move(IEngine, bytes32, uint256, uint256, uint256, uint16, uint256) external pure { // No-op } @@ -31,7 +31,7 @@ contract InvalidMove is IMoveSet { return Type.Fire; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return false; } diff --git a/test/mocks/MockEffectRemover.sol b/test/mocks/MockEffectRemover.sol index df30442f..61802680 100644 --- a/test/mocks/MockEffectRemover.sol +++ b/test/mocks/MockEffectRemover.sol @@ -10,8 +10,7 @@ import {IMoveSet} from "../../src/moves/IMoveSet.sol"; /** * Mock move that removes an effect from a target mon. - * The effect address to remove is passed as extraData (targetArgs). - * Targets the opponent's active mon. + * The effect's slot index is passed as extraData. Targets the opponent's active mon. */ contract MockEffectRemover is IMoveSet { @@ -25,20 +24,23 @@ contract MockEffectRemover is IMoveSet { uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, - uint240 extraData, + uint16 extraData, uint256 ) external { - // extraData contains the address of the effect to remove (packed as uint160) - address effectToRemove = address(uint160(extraData)); + // extraData is the slot index of the effect to remove (from getEffects). + uint256 slotIndex = uint256(extraData); // Target the opponent's active mon uint256 targetPlayerIndex = 1 - attackerPlayerIndex; - // Find and remove the effect (removeEffect in Engine handles calling onRemove with proper params) - (EffectInstance[] memory effects, uint256[] memory indices) = engine.getEffects(battleKey, targetPlayerIndex, defenderMonIndex); - for (uint256 i = 0; i < effects.length; i++) { - if (address(effects[i].effect) == effectToRemove) { - engine.removeEffect(targetPlayerIndex, defenderMonIndex, indices[i]); + // Verify the slot is still occupied at this index before removing + (EffectInstance[] memory effects, uint256[] memory indices) = + engine.getEffects(battleKey, targetPlayerIndex, defenderMonIndex); + for (uint256 i = 0; i < indices.length; i++) { + if (indices[i] == slotIndex) { + if (address(effects[i].effect) != address(0)) { + engine.removeEffect(targetPlayerIndex, defenderMonIndex, slotIndex); + } break; } } @@ -60,7 +62,7 @@ contract MockEffectRemover is IMoveSet { return MoveClass.Other; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/test/mocks/MockKVWriterMove.sol b/test/mocks/MockKVWriterMove.sol index 79640190..22222733 100644 --- a/test/mocks/MockKVWriterMove.sol +++ b/test/mocks/MockKVWriterMove.sol @@ -9,17 +9,18 @@ import {IMoveSet} from "../../src/moves/IMoveSet.sol"; import {MoveMeta} from "../../src/Structs.sol"; /// @notice Test-only move that writes a single, caller-chosen (key, value) pair to globalKV. -/// @dev extraData layout: lower 64 bits = key, upper 176 bits = value. A 0-value input -/// writes 1 by default so tests can quickly assert "something was written" without -/// encoding a non-zero value every time. +/// @dev extraData layout (16 bits total): bits 0..9 = key (≤1023), bits 10..15 = value (≤63). +/// A 0-value input writes 1 by default so tests can quickly assert "something was written" +/// without encoding a non-zero value every time. Tests that need wider key/value ranges +/// should use a different mock. contract MockKVWriterMove is IMoveSet { function name() external pure returns (string memory) { return "MockKVWriter"; } - function move(IEngine engine, bytes32, uint256, uint256, uint256, uint240 extraData, uint256) external { - uint64 key = uint64(extraData); - uint192 value = uint192(uint256(extraData) >> 64); + function move(IEngine engine, bytes32, uint256, uint256, uint256, uint16 extraData, uint256) external { + uint64 key = uint64(extraData) & 0x3FF; // 10 bits + uint192 value = uint192(uint256(extraData) >> 10); // 6 bits if (value == 0) { value = 1; } @@ -42,7 +43,7 @@ contract MockKVWriterMove is IMoveSet { return MoveClass.Self; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/test/mocks/ReduceSpAtkMove.sol b/test/mocks/ReduceSpAtkMove.sol index 375e51dc..b5738980 100644 --- a/test/mocks/ReduceSpAtkMove.sol +++ b/test/mocks/ReduceSpAtkMove.sol @@ -20,7 +20,7 @@ contract ReduceSpAtkMove is IMoveSet { return "Reduce SpAtk"; } - function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, uint240, uint256) external { + function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, uint16, uint256) external { // Get the opposing player's index uint256 opposingPlayerIndex = (attackerPlayerIndex + 1) % 2; @@ -40,7 +40,7 @@ contract ReduceSpAtkMove is IMoveSet { return Type.Math; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/test/mocks/SelfSwitchAndDamageMove.sol b/test/mocks/SelfSwitchAndDamageMove.sol index 3fb5b1d1..ac3fff8b 100644 --- a/test/mocks/SelfSwitchAndDamageMove.sol +++ b/test/mocks/SelfSwitchAndDamageMove.sol @@ -21,7 +21,7 @@ contract SelfSwitchAndDamageMove is IMoveSet { return "Self Switch And Damage Move"; } - function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, uint240 extraData, uint256) external { + function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, uint16 extraData, uint256) external { uint256 monToSwitchIndex = uint256(extraData); // Deal damage first to opponent @@ -44,7 +44,7 @@ contract SelfSwitchAndDamageMove is IMoveSet { return Type.Fire; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/test/mocks/SkipTurnMove.sol b/test/mocks/SkipTurnMove.sol index be9db1e1..9120f4ae 100644 --- a/test/mocks/SkipTurnMove.sol +++ b/test/mocks/SkipTurnMove.sol @@ -30,7 +30,7 @@ contract SkipTurnMove is IMoveSet { return "Skip Turn"; } - function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, uint240, uint256) external { + function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, uint16, uint256) external { uint256 targetIndex = (attackerPlayerIndex + 1) % 2; engine.updateMonState(targetIndex, defenderMonIndex, MonStateIndexName.ShouldSkipTurn, 1); } @@ -47,7 +47,7 @@ contract SkipTurnMove is IMoveSet { return TYPE; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/test/mocks/StatBoostsMove.sol b/test/mocks/StatBoostsMove.sol index 2b869396..4abc7544 100644 --- a/test/mocks/StatBoostsMove.sol +++ b/test/mocks/StatBoostsMove.sol @@ -22,12 +22,12 @@ contract StatBoostsMove is IMoveSet { return ""; } - function move(IEngine engine, bytes32, uint256, uint256, uint256, uint240 extraData, uint256) external { - // Unpack extraData: lower 60 bits = playerIndex, next 60 bits = monIndex, next 60 bits = statIndex, upper 60 bits = boostAmount - uint256 playerIndex = uint256(extraData) & ((1 << 60) - 1); - uint256 monIndex = (uint256(extraData) >> 60) & ((1 << 60) - 1); - uint256 statIndex = (uint256(extraData) >> 120) & ((1 << 60) - 1); - int32 boostAmount = int32(int256((uint256(extraData) >> 180) & ((1 << 60) - 1))); + function move(IEngine engine, bytes32, uint256, uint256, uint256, uint16 extraData, uint256) external { + // Unpack extraData: [boostAmount:8 | statIndex:4 | monIndex:3 | playerIndex:1] + uint256 playerIndex = uint256(extraData) & 0x1; + uint256 monIndex = (uint256(extraData) >> 1) & 0x7; + uint256 statIndex = (uint256(extraData) >> 4) & 0xF; + int32 boostAmount = int32(int8(uint8(uint256(extraData) >> 8))); // For all tests, we'll use Temp stat boosts with Multiply type for positive boosts // and Divide type for negative boosts @@ -59,7 +59,7 @@ contract StatBoostsMove is IMoveSet { return Type.Air; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/test/mocks/TestMoveFactory.sol b/test/mocks/TestMoveFactory.sol index 230d654f..02ab8f1d 100644 --- a/test/mocks/TestMoveFactory.sol +++ b/test/mocks/TestMoveFactory.sol @@ -24,7 +24,7 @@ contract TestMove is IMoveSet { return "Test Move"; } - function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, uint240, uint256) external { + function move(IEngine engine, bytes32, uint256 attackerPlayerIndex, uint256, uint256 defenderMonIndex, uint16, uint256) external { uint256 opponentIndex = (attackerPlayerIndex + 1) % 2; engine.dealDamage(opponentIndex, defenderMonIndex, _damage); } @@ -41,7 +41,7 @@ contract TestMove is IMoveSet { return _moveType; } - function isValidTarget(IEngine, bytes32, uint240) external pure returns (bool) { + function isValidTarget(IEngine, bytes32, uint16) external pure returns (bool) { return true; } diff --git a/test/mons/AuroxTest.sol b/test/mons/AuroxTest.sol index 89914532..bcd566fd 100644 --- a/test/mons/AuroxTest.sol +++ b/test/mons/AuroxTest.sol @@ -87,7 +87,7 @@ contract AuroxTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses Bull Rush, Bob does nothing _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, 0, 0); @@ -177,7 +177,7 @@ contract AuroxTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice spends 1 stamina, Bob inflicts frostbite @@ -198,7 +198,7 @@ contract AuroxTest is Test, BattleHelper { ); // Alice swaps to mon index 1, Bob does the 50% attack - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 1, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 1, uint16(1), 0); // Verify that Alice's mon index 1 has taken 50% damage int32 aliceDamage = engine.getMonStateForBattle(battleKey, 0, 1, MonStateIndexName.Hp); @@ -209,7 +209,7 @@ contract AuroxTest is Test, BattleHelper { ); // Alice uses Gilded Recovery targeting self, Bob does nothing - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 2, NO_OP_MOVE_INDEX, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 2, NO_OP_MOVE_INDEX, uint16(1), 0); // Nothing should happen, mon index 0 for Alice should still have -1 staminaDelta, hpDelta for mon index 1 should still be the same assertEq( @@ -224,7 +224,7 @@ contract AuroxTest is Test, BattleHelper { ); // Alice uses Gilded Recovery targeting mon index , Bob does nothing - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 2, NO_OP_MOVE_INDEX, uint240(0), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 2, NO_OP_MOVE_INDEX, uint16(0), 0); // Verify that Alice's mon index 1 is healed by 50% and mon index 0 has staminaDelta of 0, and no longer has frostbite assertEq( @@ -293,7 +293,7 @@ contract AuroxTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses Iron Wall, Bob does nothing @@ -326,7 +326,7 @@ contract AuroxTest is Test, BattleHelper { assertEq(aliceDamage, expectedDamagePerHit * 2, "Alice's mon should take reduced damage on second hit"); // Alice switches to mon index 1, Bob does nothing - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(1), 0); // Verify that the Iron Wall effect is now gone after switch out (effects, ) = engine.getEffects(battleKey, 0, 0); @@ -380,7 +380,7 @@ contract AuroxTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses Iron Wall, Bob does nothing @@ -443,7 +443,7 @@ contract AuroxTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice does nothing, Bob attacks @@ -518,7 +518,7 @@ contract AuroxTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice does nothing, Bob attacks @@ -613,7 +613,7 @@ contract AuroxTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses attack, Bob does nothing @@ -624,7 +624,7 @@ contract AuroxTest is Test, BattleHelper { assertEq(bobAttackDelta, int32(int8(upOnly.ATTACK_BOOST_PERCENT())) * int32(maxHp) / 100, "Bob's mon should be boosted"); // Alice does nothing, Bob switches to mon index 1 - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint240(1)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint16(1)); // Verify that Bob's mon index 0 has a positive attack delta of upOnly.ATTACK_BOOST_PERCENT() bobAttackDelta = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Attack); @@ -699,7 +699,7 @@ contract AuroxTest is Test, BattleHelper { // Turn 0: Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Turn 1: Alice does nothing, Bob attacks Alice @@ -760,7 +760,7 @@ contract AuroxTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Set rng to be 2 to trigger frostbite diff --git a/test/mons/EkinekiTest.sol b/test/mons/EkinekiTest.sol index 7a4315fd..9bcca4ab 100644 --- a/test/mons/EkinekiTest.sol +++ b/test/mons/EkinekiTest.sol @@ -121,7 +121,7 @@ contract EkinekiTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses Bubble Bop, Bob does nothing @@ -147,7 +147,7 @@ contract EkinekiTest is Test, BattleHelper { bytes32 battleKey2 = _startBattle(validator2, engine2, mockOracle, registry2, matchmaker2, address(commitManager2)); _commitRevealExecuteForAliceAndBob( - engine2, commitManager2, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine2, commitManager2, battleKey2, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Bob uses single hit on Alice @@ -194,11 +194,11 @@ contract EkinekiTest is Test, BattleHelper { // Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses SneakAttack targeting Bob's non-active mon (index 1) - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(1), 0); // Verify Bob's mon at index 1 (non-active) took damage int32 bobMon1HpDelta = engine.getMonStateForBattle(battleKey, 1, 1, MonStateIndexName.Hp); @@ -241,17 +241,17 @@ contract EkinekiTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses SneakAttack targeting Bob's non-active mon (index 1) - first use - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(1), 0); int32 bobMon1DamageAfterFirst = engine.getMonStateForBattle(battleKey, 1, 1, MonStateIndexName.Hp); assertTrue(bobMon1DamageAfterFirst < 0, "First sneak attack should deal damage"); // Alice uses SneakAttack again - should do nothing (already used this switch-in) - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(1), 0); int32 bobMon1DamageAfterSecond = engine.getMonStateForBattle(battleKey, 1, 1, MonStateIndexName.Hp); assertEq( @@ -293,21 +293,21 @@ contract EkinekiTest is Test, BattleHelper { // Both players select mon 0 _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses SneakAttack - first use works - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(1), 0); int32 damageAfterFirst = engine.getMonStateForBattle(battleKey, 1, 1, MonStateIndexName.Hp); assertTrue(damageAfterFirst < 0, "First sneak attack should deal damage"); // Alice switches to mon 1 (sneak attack effect removed on switch-out) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(1), 0 + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(1), 0 ); // Alice (now mon 1) uses SneakAttack again - should work (reset by switch) - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(1), 0); int32 damageAfterReset = engine.getMonStateForBattle(battleKey, 1, 1, MonStateIndexName.Hp); assertTrue(damageAfterReset < damageAfterFirst, "Sneak attack should work again after switching"); } @@ -362,7 +362,7 @@ contract EkinekiTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Bob uses test attack without 999 (baseline) @@ -457,7 +457,7 @@ contract EkinekiTest is Test, BattleHelper { // Both select mon 0 _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Bob attacks Alice's mon 0 (KO), Alice does nothing @@ -473,7 +473,7 @@ contract EkinekiTest is Test, BattleHelper { // Alice forced switch to mon 2 (the one with savior complex) // After KO, playerSwitchForTurnFlag = 0 (Alice must switch, no commit needed) vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, "", uint240(2), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(2), true); engine.resetCallContext(); // Verify that Alice's mon 2 got a sp atk boost (STAGE_1_BOOST = 15% of 100 = 15) int32 spAtkDelta = engine.getMonStateForBattle(battleKey, 0, 2, MonStateIndexName.SpecialAttack); @@ -556,7 +556,7 @@ contract EkinekiTest is Test, BattleHelper { // Both select mon 0 _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Bob KOs Alice's mon 0 @@ -564,14 +564,14 @@ contract EkinekiTest is Test, BattleHelper { // Alice forced switch to mon 1 (savior complex triggers with 1 KO) vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, "", uint240(1), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(1), true); engine.resetCallContext(); int32 spAtkDeltaFirstSwitch = engine.getMonStateForBattle(battleKey, 0, 1, MonStateIndexName.SpecialAttack); assertEq(spAtkDeltaFirstSwitch, 15, "Should get 15 sp atk boost from 1 KO"); // Alice switches to mon 2, Bob KOs Alice's mon 2 in the same turn (Bob is faster but switch has higher priority) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint240(2), 0 + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint16(2), 0 ); // Verify Alice's mon 2 is KO'd @@ -583,7 +583,7 @@ contract EkinekiTest is Test, BattleHelper { // Alice forced switch back to mon 1 (savior complex should NOT trigger again) vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, "", uint240(1), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(1), true); engine.resetCallContext(); int32 spAtkDeltaSecondSwitch = engine.getMonStateForBattle(battleKey, 0, 1, MonStateIndexName.SpecialAttack); // Boost is temp so it was cleared when mon 1 switched out, and savior complex @@ -665,7 +665,7 @@ contract EkinekiTest is Test, BattleHelper { // Alice selects mon 0 (with savior complex) - no KO'd mons _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Verify no boost was applied (0 KOs) @@ -682,7 +682,7 @@ contract EkinekiTest is Test, BattleHelper { // Alice forced switch to mon 1 vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, "", uint240(1), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(1), true); engine.resetCallContext(); // Mon 1 has no ability, so no savior complex trigger // But the savior complex on mon 0 should NOT have been consumed (it didn't trigger) @@ -720,7 +720,7 @@ contract EkinekiTest is Test, BattleHelper { _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses Overflow, Bob does nothing diff --git a/test/mons/EmbursaTest.sol b/test/mons/EmbursaTest.sol index e80a42df..d5dae67c 100644 --- a/test/mons/EmbursaTest.sol +++ b/test/mons/EmbursaTest.sol @@ -94,12 +94,12 @@ contract EmbursaTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses Q5, Bob does nothing _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); // Verify no damage occurred @@ -110,7 +110,7 @@ contract EmbursaTest is Test, BattleHelper { // Wait 4 turns for (uint256 i = 0; i < 4; i++) { _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, NO_OP_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, NO_OP_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); } // Verify no damage occurred @@ -123,7 +123,7 @@ contract EmbursaTest is Test, BattleHelper { // Alice and Bob both do nothing _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, NO_OP_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, NO_OP_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); // Verify damage occurred @@ -226,7 +226,7 @@ contract EmbursaTest is Test, BattleHelper { vm.warp(vm.getBlockTimestamp() + 1); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, 0, 0); (EffectInstance[] memory effects, ) = engine.getEffects(battleKey, 1, 0); @@ -259,7 +259,7 @@ contract EmbursaTest is Test, BattleHelper { vm.warp(vm.getBlockTimestamp() + 1); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); (effects, ) = engine.getEffects(battleKey, 1, 0); assertEq(effects.length, 0, "Bob's mon should have no effects"); @@ -276,7 +276,7 @@ contract EmbursaTest is Test, BattleHelper { // Verify Q5 was applied to global effects, verify Alice is KOed battleKey = _startBattle(validatorToUse, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, 0, 0); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 1, 4, 0, 0); @@ -295,7 +295,7 @@ contract EmbursaTest is Test, BattleHelper { // Verify Honey Bribe applied stat boost to Bob's mon, verify Alice is KOed battleKey = _startBattle(validatorToUse, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, 0, 0); _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 3, 4, 0, 0); @@ -372,7 +372,7 @@ contract EmbursaTest is Test, BattleHelper { // Both switch in _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses Q5, Bob does nothing @@ -381,7 +381,7 @@ contract EmbursaTest is Test, BattleHelper { // Wait 4 turns (Q5 counter ticks from 1 to 5) for (uint256 i = 0; i < 4; i++) { _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, NO_OP_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, NO_OP_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); } @@ -469,7 +469,7 @@ contract EmbursaTest is Test, BattleHelper { vm.warp(vm.getBlockTimestamp() + 1); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Set RNG so that burn triggers (rng % 3 == 2) @@ -555,7 +555,7 @@ contract EmbursaTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validatorToUse, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Bob uses burn attack on Alice (Bob is faster) diff --git a/test/mons/GhouliathTest.sol b/test/mons/GhouliathTest.sol index 2a12be8d..c94c1ce6 100644 --- a/test/mons/GhouliathTest.sol +++ b/test/mons/GhouliathTest.sol @@ -150,7 +150,7 @@ contract GhouliathTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Bob uses the attack (which KOs) on Alice's mon @@ -170,7 +170,7 @@ contract GhouliathTest is Test, BattleHelper { // Alice swaps in mon index 1 vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), true); engine.resetCallContext(); // We wait for the REVIVAL_DELAY - 1 turns to pass for (uint256 i = 0; i < riseFromTheGrave.REVIVAL_DELAY() - 1; i++) { @@ -188,12 +188,12 @@ contract GhouliathTest is Test, BattleHelper { assertEq(damageTaken, -99, "Alice's mon should have 1 HP"); // Alice swaps in mon index 0, Bob does attack again, which KOs Alice's mon - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint240(0), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, 0, uint16(0), 0); // Verify the mon is not revived after REVIVAL_DELAY turns // (First we swap in mon index 1) vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), true); engine.resetCallContext(); for (uint256 i = 0; i < riseFromTheGrave.REVIVAL_DELAY() - 1; i++) { _commitRevealExecuteForAliceAndBob( @@ -256,7 +256,7 @@ contract GhouliathTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Bob uses the attack (which KOs) on Alice's mon @@ -264,14 +264,14 @@ contract GhouliathTest is Test, BattleHelper { // Alice swaps in mon index 1 vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), true); engine.resetCallContext(); // Alice KOs Bob's mon _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, 0, 0); // Bob swaps in mon index 1 vm.startPrank(BOB); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), true); engine.resetCallContext(); // We wait for the REVIVAL_DELAY turns to pass for (uint256 i = 0; i < riseFromTheGrave.REVIVAL_DELAY() - 1; i++) { @@ -346,7 +346,7 @@ contract GhouliathTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses WitherAway on Bob's mon @@ -448,7 +448,7 @@ contract GhouliathTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses Osteoporosis on Bob's mon @@ -500,12 +500,12 @@ contract GhouliathTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice does nothing, Bob switches to mon index 1 _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint240(1) + engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint16(1) ); // Alice uses Eternal Grudge on Bob's mon diff --git a/test/mons/GorillaxTest.sol b/test/mons/GorillaxTest.sol index 8373d33d..9093a41f 100644 --- a/test/mons/GorillaxTest.sol +++ b/test/mons/GorillaxTest.sol @@ -97,7 +97,7 @@ contract GorillaxTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice chooses to attack, Bob chooses to do nothing for CHARGE_COUNT rounds @@ -168,12 +168,12 @@ contract GorillaxTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses Rock Pull, Bob switches to mon index 1 _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 0, SWITCH_MOVE_INDEX, uint240(0), uint240(1) + engine, commitManager, battleKey, 0, SWITCH_MOVE_INDEX, uint16(0), uint16(1) ); // Assert that Bob's mon index 0 took damage @@ -184,7 +184,7 @@ contract GorillaxTest is Test, BattleHelper { // Alice uses Rock Pull, Bob does not switch _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); // Assert that Alice's mon index 0 took damage diff --git a/test/mons/IblivionTest.sol b/test/mons/IblivionTest.sol index 56fcb7d9..e8bd5659 100644 --- a/test/mons/IblivionTest.sol +++ b/test/mons/IblivionTest.sol @@ -112,7 +112,7 @@ contract IblivionTest is Test, BattleHelper { // Switch in mons _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Check that Baselight level is 2 (1 from switch-in + 1 from round end) @@ -153,7 +153,7 @@ contract IblivionTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Baselight starts at 2 (1 from switch + 1 from round end) @@ -227,7 +227,7 @@ contract IblivionTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice starts with 2 Baselight stacks (1 from switch + 1 from round end) @@ -292,7 +292,7 @@ contract IblivionTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // No Baselight stacks @@ -370,7 +370,7 @@ contract IblivionTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice has 2 stacks (not 3), so normal power (80) @@ -441,7 +441,7 @@ contract IblivionTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Wait for 3 stacks (start at 2, need 1 more round) @@ -500,7 +500,7 @@ contract IblivionTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // At Baselight 2, Loop should give 30% boost @@ -555,7 +555,7 @@ contract IblivionTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses Loop first time @@ -611,7 +611,7 @@ contract IblivionTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Wait for 3 stacks (start at 2, need 1 more round) @@ -666,7 +666,7 @@ contract IblivionTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Start at 2 stacks (1 initial + 1 from round end) @@ -714,7 +714,7 @@ contract IblivionTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses Loop to get stat boosts @@ -767,7 +767,7 @@ contract IblivionTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses Loop @@ -847,7 +847,7 @@ contract IblivionTest is Test, BattleHelper { bytes32 battleKey = _startBattle(validator, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Check priority @@ -945,7 +945,7 @@ contract IblivionTest is Test, BattleHelper { // Switch in mons _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Verify Alice's attack starts at base (0 delta) @@ -994,10 +994,20 @@ contract IblivionTest is Test, BattleHelper { } assertTrue(stillHasBurn, "Alice should still have burn effect after Renormalize"); - // Bob uses MockEffectRemover to remove burn from Alice (move index 1) - // Pass burn status address as extraData + // Bob uses MockEffectRemover to remove burn from Alice (move index 1). + // Pass burn status's slot index as extraData (looked up from Alice's effects). + uint16 burnSlot; + { + (EffectInstance[] memory beforeRemove, uint256[] memory beforeIndices) = engine.getEffects(battleKey, 0, 0); + for (uint256 i = 0; i < beforeRemove.length; i++) { + if (address(beforeRemove[i].effect) == address(burnStatus)) { + burnSlot = uint16(beforeIndices[i]); + break; + } + } + } _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 1, 0, uint240(uint160(address(burnStatus))) + engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 1, 0, burnSlot ); // Verify burn effect is removed diff --git a/test/mons/InutiaTest.sol b/test/mons/InutiaTest.sol index 57434918..8c1f37a0 100644 --- a/test/mons/InutiaTest.sol +++ b/test/mons/InutiaTest.sol @@ -112,7 +112,7 @@ contract InutiaTest is Test, BattleHelper { // First move: Alice switches to the mon with Interweaving ability, Bob switches to the other mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(1), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(1), uint16(0) ); // Check that Bob's mon Attack stat has been decreased @@ -124,7 +124,7 @@ contract InutiaTest is Test, BattleHelper { // Alice switches back to the regular mon, Bob does a No-Op _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(0), 0 + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(0), 0 ); // Check that Bob's mon SpecialAttack stat has been decreased @@ -177,7 +177,7 @@ contract InutiaTest is Test, BattleHelper { // Send in mons _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both players select move index 0 @@ -205,7 +205,7 @@ contract InutiaTest is Test, BattleHelper { // Now both players swap to mon index 1 _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(1), uint240(1) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(1), uint16(1) ); // The stat boost should carry over @@ -220,7 +220,7 @@ contract InutiaTest is Test, BattleHelper { // Now both players swap back to mon index 0 _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both players select move index 0 @@ -326,7 +326,7 @@ contract InutiaTest is Test, BattleHelper { bytes32 battleKey = _startBattle(v, engine, mockOracle, defaultRegistry, matchmaker, address(commitManager)); _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses CE, Bob does nothing (turn 1) @@ -339,7 +339,7 @@ contract InutiaTest is Test, BattleHelper { // Bob swaps to mon index 1 (turn 3) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint240(1) + engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint16(1) ); // Verify damage dealt to Bob's mon index 1 is 1/16 of max HP (charge 1) @@ -348,7 +348,7 @@ contract InutiaTest is Test, BattleHelper { // Bob swaps to mon index 2 (turn 4) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint240(2) + engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint16(2) ); // Verify damage dealt to Bob's mon index 2 is 1/4 of max HP (charge 2) @@ -357,7 +357,7 @@ contract InutiaTest is Test, BattleHelper { // Bob swaps back to mon index 0 (turn 5) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint240(0) + engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint16(0) ); // Verify damage dealt to Bob's mon index 0 is 1/8 of max HP (charge 3) @@ -366,7 +366,7 @@ contract InutiaTest is Test, BattleHelper { // Bob swaps to mon index 1 _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint240(1) + engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, 0, uint16(1) ); // Verify damage dealt to Bob's mon index 1 is 1/16 of max HP (charge 4) @@ -391,12 +391,12 @@ contract InutiaTest is Test, BattleHelper { // Alice swaps to mon index 1, Bob does nothing _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(1), 0 + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(1), 0 ); // Alice swaps back to mon index 0, Bob does nothing _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(0), 0 + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(0), 0 ); // Verify Alice's mon index 0 is healed by 1/16 of max HP diff --git a/test/mons/MalalienTest.sol b/test/mons/MalalienTest.sol index 63557fbb..569b76d6 100644 --- a/test/mons/MalalienTest.sol +++ b/test/mons/MalalienTest.sol @@ -129,7 +129,7 @@ contract MalalienTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice's mon has ActusReus ability @@ -153,7 +153,7 @@ contract MalalienTest is Test, BattleHelper { // Bob switches to mon index 1 vm.startPrank(BOB); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), true); engine.resetCallContext(); // Alice does nothing, Bob attacks and KOs Alice's mon _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, NO_OP_MOVE_INDEX, 0, 0, 0); @@ -208,11 +208,11 @@ contract MalalienTest is Test, BattleHelper { // Alice and Bob both send in mon index 0 _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both players use triple think - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, 0, uint240(0), uint240(0)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, 0, uint16(0), uint16(0)); // SpecialAttack delta for both is 100 int32 aliceSpAtkBoost = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.SpecialAttack); @@ -223,7 +223,7 @@ contract MalalienTest is Test, BattleHelper { // Alice uses it again, Bob swaps out _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 0, SWITCH_MOVE_INDEX, uint240(0), uint240(1) + engine, commitManager, battleKey, 0, SWITCH_MOVE_INDEX, uint16(0), uint16(1) ); // Alice should be at 1.75 * 1.75 = 1.225 + 1.75 + 0.0875 = 2.0625, Bob should be at 0 diff --git a/test/mons/PengymTest.sol b/test/mons/PengymTest.sol index 6346bd03..8c0e75c0 100644 --- a/test/mons/PengymTest.sol +++ b/test/mons/PengymTest.sol @@ -180,7 +180,7 @@ contract PengymTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Check that Alice's mon has the PostWorkout effect @@ -216,12 +216,12 @@ contract PengymTest is Test, BattleHelper { // Alice switches to her second mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(1), 0 + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(1), 0 ); // Alice switches back to her first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(0), 0 + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(0), 0 ); // Check that Alice's mon no longer has the PanicStatus effect @@ -355,7 +355,7 @@ contract PengymTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Check that Alice's mon has the PostWorkout effect @@ -396,12 +396,12 @@ contract PengymTest is Test, BattleHelper { // Alice switches to her second mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(1), 0 + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(1), 0 ); // Alice switches back to her first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(0), 0 + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(0), 0 ); // Check that Alice's mon no longer has the FrostbiteStatus effect @@ -472,25 +472,25 @@ contract PengymTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice deals damage to Bob, record the damage dealt _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 1, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, 1, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); int32 deepFreezeDamage = -1 * engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Hp); // Alice inflicts frostbite on Bob, Bob does nothing _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); int32 bobDamageBefore = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Hp); // Alice uses deep freeze, record the damage dealt _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 1, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, 1, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); int32 bobDamageAfter = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Hp); @@ -576,11 +576,11 @@ contract PengymTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice selects pistol squat, Bob selects move index 1 and outspeeds - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, 1, uint240(0), uint240(0)); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, 1, uint16(0), uint16(0)); // Alice should be KO'ed int32 koFlag = engine.getMonStateForBattle(battleKey, 0, 0, MonStateIndexName.IsKnockedOut); @@ -588,11 +588,11 @@ contract PengymTest is Test, BattleHelper { // Alice swaps to mon index 1 vm.startPrank(ALICE); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), true); engine.resetCallContext(); // Alice selects pistol squat, Bob does nothing _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); // Active mon for Bob should be 1 @@ -601,7 +601,7 @@ contract PengymTest is Test, BattleHelper { // Alice selects pistol squat, Bob does nothing _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); // Active mon for Bob should be 0 @@ -610,7 +610,7 @@ contract PengymTest is Test, BattleHelper { // Alice selects pistol squat, Bob does nothing (and dies) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); bobActiveMonIndex = engine.getActiveMonIndexForBattleState(battleKey)[1]; @@ -618,21 +618,21 @@ contract PengymTest is Test, BattleHelper { // Bob sends in mon index 1 vm.startPrank(BOB); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(1), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(1), true); engine.resetCallContext(); // Alice selects pistol squat, Bob does nothing _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); // Now Bob's mon index 1 is KOed // Bob sends in mon index 2 vm.startPrank(BOB); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint240(2), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, 0, uint16(2), true); engine.resetCallContext(); // Alice selects pistol squat, Bob does nothing _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); // Now Bob has mon index 2 (already took damage) and mon index 3 @@ -641,21 +641,21 @@ contract PengymTest is Test, BattleHelper { // Bob switches back to mon index 2, Alice does nothing _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(2) + engine, commitManager, battleKey, NO_OP_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(2) ); // Alice KOs Bob's mon index 2 _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 1, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, 1, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); // Bob sends in mon index 3 vm.startPrank(BOB); - commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, "", uint240(3), true); + commitManager.revealMove(battleKey, SWITCH_MOVE_INDEX, uint104(0), uint16(3), true); engine.resetCallContext(); // Alice tries to force a switch, but active mon should not change _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(0), uint16(0) ); bobActiveMonIndex = engine.getActiveMonIndexForBattleState(battleKey)[1]; assertEq(bobActiveMonIndex, 3, "No mons left"); diff --git a/test/mons/SofabbiTest.sol b/test/mons/SofabbiTest.sol index ab64d93d..0221faf7 100644 --- a/test/mons/SofabbiTest.sol +++ b/test/mons/SofabbiTest.sol @@ -108,7 +108,7 @@ contract SofabbiTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Verify that the CarrotHarvest effect was applied to Alice's mon @@ -117,12 +117,12 @@ contract SofabbiTest is Test, BattleHelper { // Now have Alice switch to her second mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(1), 0 + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(1), 0 ); // Now have Alice switch back to her first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(0), 0 + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(0), 0 ); // Verify that the CarrotHarvest effect is still only applied once @@ -188,7 +188,7 @@ contract SofabbiTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Verify that staminaDelta is 1 for both mons @@ -297,28 +297,28 @@ contract SofabbiTest is Test, BattleHelper { // First move: Both players select their first mon (index 0, the Air mon) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Damage is returned in negative, so that's why there are some weird sign cancellations below // Alice uses Guest Feature targeting mon index 1, it should deal 2x damage _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(1), uint240(0) + engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(1), uint16(0) ); int32 bobDmg = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Hp); assertApproxEqRel(-1 * bobDmg, int32(2 * gf.BASE_POWER()), 2e17); // Alice uses Guest Feature targeting mon index 2, it should deal 0 damage _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(2), uint240(0) + engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(2), uint16(0) ); int32 newBobDmg = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Hp); assertEq(newBobDmg, bobDmg, "No damage"); // Alice uses Guest Feature targeting mon index 3, it should deal 1/2 damage _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(3), uint240(0) + engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(3), uint16(0) ); newBobDmg = engine.getMonStateForBattle(battleKey, 1, 0, MonStateIndexName.Hp); bobDmg = bobDmg - newBobDmg; @@ -377,7 +377,7 @@ contract SofabbiTest is Test, BattleHelper { // Both players send in mon index 0 _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses nothing, Bob uses move index 1, puts Alice at 1 HP @@ -454,7 +454,7 @@ contract SofabbiTest is Test, BattleHelper { // Both players send in mon index 0 _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Set rng to be 10 diff --git a/test/mons/VolthareTest.sol b/test/mons/VolthareTest.sol index 7991286b..3f222e21 100644 --- a/test/mons/VolthareTest.sol +++ b/test/mons/VolthareTest.sol @@ -124,7 +124,7 @@ contract VolthareTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Verify that Bob's mon took damage from PreemptiveShock @@ -207,11 +207,11 @@ contract VolthareTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses the Overclock move (move index 0), Bob does nothing - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint240(0), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 0, NO_OP_MOVE_INDEX, uint16(0), 0); // Verify that Overclock is applied (EffectInstance[] memory effects,) = engine.getEffects(battleKey, 2, 0); @@ -222,7 +222,7 @@ contract VolthareTest is Test, BattleHelper { mockOracle.setRNG(2); // Alice uses Mega Star Blast (move index 1), Bob does nothing - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 1, NO_OP_MOVE_INDEX, uint240(0), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 1, NO_OP_MOVE_INDEX, uint16(0), 0); // Verify that Bob's mon is zapped (effects,) = engine.getEffects(battleKey, 1, 0); @@ -237,7 +237,7 @@ contract VolthareTest is Test, BattleHelper { mockOracle.setRNG(msb.BASE_ACCURACY() + 1); // Alice uses Mega Star Blast, Bob does nothing - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 1, NO_OP_MOVE_INDEX, uint240(0), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 1, NO_OP_MOVE_INDEX, uint16(0), 0); // Verify that Bob's mon is not zapped (again) (effects,) = engine.getEffects(battleKey, 1, 0); @@ -303,7 +303,7 @@ contract VolthareTest is Test, BattleHelper { // Both players send in their mons _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Bob applies Overclock (stored with player index 1). Alice does nothing. @@ -320,7 +320,7 @@ contract VolthareTest is Test, BattleHelper { mockOracle.setRNG(msb.BASE_ACCURACY() + 1); // Alice uses MegaStarBlast. Bob does nothing. - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 1, NO_OP_MOVE_INDEX, uint240(0), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 1, NO_OP_MOVE_INDEX, uint16(0), 0); // Overclock should still be present (Alice cannot clear Bob's Overclock) (effects,) = engine.getEffects(battleKey, 2, 0); @@ -396,7 +396,7 @@ contract VolthareTest is Test, BattleHelper { // First move: Both players select their first mon (index 0) _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Both players use Dual Shock, Alice should move first and skip their next move diff --git a/test/mons/XmonTest.sol b/test/mons/XmonTest.sol index 10b041b6..8c3d995d 100644 --- a/test/mons/XmonTest.sol +++ b/test/mons/XmonTest.sol @@ -87,7 +87,7 @@ contract XmonTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses Contagious Slumber, Bob does nothing @@ -160,7 +160,7 @@ contract XmonTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Set RNG to guarantee stamina steal (>= 50) @@ -219,7 +219,7 @@ contract XmonTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Alice uses Somniphobia, Bob uses Somniphobia too @@ -337,7 +337,7 @@ contract XmonTest is Test, BattleHelper { // Alice sends in fast mon, Bob sends in slow mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(1) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(1) ); // Verify that Alice has the Dreamcatcher effect @@ -400,7 +400,7 @@ contract XmonTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Turn 1: Alice uses Night Terrors, Bob does nothing @@ -467,7 +467,7 @@ contract XmonTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Turn 1: Alice uses Night Terrors, Bob does nothing @@ -485,7 +485,7 @@ contract XmonTest is Test, BattleHelper { assertTrue(hasNightTerrorsBeforeSwap, "Alice's mon 0 should have Night Terrors effect before swap"); // Turn 2: Alice swaps to mon 1, Bob does nothing - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(1), 0); // Verify Alice's mon 0 no longer has Night Terrors effect (EffectInstance[] memory aliceEffectsAfterSwap, ) = engine.getEffects(battleKey, 0, 0); @@ -555,7 +555,7 @@ contract XmonTest is Test, BattleHelper { // Both players select their first mon _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Set RNG to 1 to prevent early waking from sleep (rng % 3 == 0 wakes early) @@ -569,10 +569,10 @@ contract XmonTest is Test, BattleHelper { int32 awakeDamage = -bobHpAfterAwakeDamage; // Turn 2: Alice swaps out to mon 1 to clear Night Terrors, Bob does nothing - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(1), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(1), 0); // Turn 3: Alice swaps back to mon 0, Bob does nothing - _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint240(0), 0); + _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, SWITCH_MOVE_INDEX, NO_OP_MOVE_INDEX, uint16(0), 0); // Turn 4: Alice uses Sleep move on Bob, Bob does nothing _commitRevealExecuteForAliceAndBob(engine, commitManager, battleKey, 1, NO_OP_MOVE_INDEX, 0, 0); diff --git a/test/moves/StandardAttackRngTest.sol b/test/moves/StandardAttackRngTest.sol index d06be33b..836976cc 100644 --- a/test/moves/StandardAttackRngTest.sol +++ b/test/moves/StandardAttackRngTest.sol @@ -93,7 +93,7 @@ contract StandardAttackRngTest is Test, BattleHelper { // Switch in mon 0 on both sides. _commitRevealExecuteForAliceAndBob( - engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0) + engine, commitManager, battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0) ); // Oracle returns the same rng for both attackers this turn. From e2500a5729f4ca42028105c8e0f6717b3e6692a6 Mon Sep 17 00:00:00 2001 From: owen <22165146+sudo-owen@users.noreply.github.com> Date: Mon, 4 May 2026 16:19:36 -0700 Subject: [PATCH 10/10] clean up --- script/EngineAndPeriphery.s.sol | 4 -- src/Engine.sol | 1 - src/moves/StandardAttack.sol | 1 - test/BatchInstrumentationTest.sol | 75 ++++++++++--------------------- test/EngineGlobalKVTest.sol | 1 - test/GachaTest.sol | 1 - test/InlineEngineGasTest.sol | 1 - test/SignedCommitManager.t.sol | 1 - test/StandardAttackPvPGasTest.sol | 1 - test/mons/PengymTest.sol | 12 ++--- 10 files changed, 29 insertions(+), 69 deletions(-) diff --git a/script/EngineAndPeriphery.s.sol b/script/EngineAndPeriphery.s.sol index 2a4eb4bd..8a041b7d 100644 --- a/script/EngineAndPeriphery.s.sol +++ b/script/EngineAndPeriphery.s.sol @@ -6,15 +6,11 @@ import "../src/Constants.sol"; // Fundamental entities import {SignedCommitManager} from "../src/commit-manager/SignedCommitManager.sol"; -import {DefaultRuleset} from "../src/DefaultRuleset.sol"; import {Engine} from "../src/Engine.sol"; import {DefaultValidator} from "../src/DefaultValidator.sol"; import {OkayCPU} from "../src/cpu/OkayCPU.sol"; import {BetterCPU} from "../src/cpu/BetterCPU.sol"; -import {IEffect} from "../src/effects/IEffect.sol"; -import {StaminaRegen} from "../src/effects/StaminaRegen.sol"; import {GachaRegistry, IGachaRNG} from "../src/gacha/GachaRegistry.sol"; -import {DefaultRandomnessOracle} from "../src/rng/DefaultRandomnessOracle.sol"; import {ICPURNG} from "../src/rng/ICPURNG.sol"; import {DefaultMonRegistry} from "../src/teams/DefaultMonRegistry.sol"; import {GachaTeamRegistry} from "../src/teams/GachaTeamRegistry.sol"; diff --git a/src/Engine.sol b/src/Engine.sol index 4a42d10d..df1be320 100644 --- a/src/Engine.sol +++ b/src/Engine.sol @@ -15,7 +15,6 @@ import {StaminaRegenLogic} from "./lib/StaminaRegenLogic.sol"; import {TimeoutCheckParams, ValidatorLogic} from "./lib/ValidatorLogic.sol"; import {IMatchmaker} from "./matchmaker/IMatchmaker.sol"; import {AttackCalculator} from "./moves/AttackCalculator.sol"; -import {MoveSlotLib} from "./moves/MoveSlotLib.sol"; import {TypeCalcLib} from "./types/TypeCalcLib.sol"; contract Engine is IEngine, MappingAllocator { diff --git a/src/moves/StandardAttack.sol b/src/moves/StandardAttack.sol index 66a464d6..18223e9e 100644 --- a/src/moves/StandardAttack.sol +++ b/src/moves/StandardAttack.sol @@ -10,7 +10,6 @@ import {IEffect} from "../effects/IEffect.sol"; import {Ownable} from "../lib/Ownable.sol"; import {ITypeCalculator} from "../types/ITypeCalculator.sol"; -import {AttackCalculator} from "./AttackCalculator.sol"; import {IMoveSet} from "./IMoveSet.sol"; import {ATTACK_PARAMS} from "./StandardAttackStructs.sol"; diff --git a/test/BatchInstrumentationTest.sol b/test/BatchInstrumentationTest.sol index 6119dbf6..1d1fb6d2 100644 --- a/test/BatchInstrumentationTest.sol +++ b/test/BatchInstrumentationTest.sol @@ -8,12 +8,10 @@ import "../src/Structs.sol"; import {Engine} from "../src/Engine.sol"; import {SignedCommitManager} from "../src/commit-manager/SignedCommitManager.sol"; -import {SignedCommitLib} from "../src/commit-manager/SignedCommitLib.sol"; import {SignedMatchmaker} from "../src/matchmaker/SignedMatchmaker.sol"; import {BattleOfferLib} from "../src/matchmaker/BattleOfferLib.sol"; import {StandardAttackFactory} from "../src/moves/StandardAttackFactory.sol"; import {ATTACK_PARAMS} from "../src/moves/StandardAttackStructs.sol"; -import {EIP712} from "../src/lib/EIP712.sol"; import {IEngine} from "../src/IEngine.sol"; import {IEngineHook} from "../src/IEngineHook.sol"; @@ -26,11 +24,12 @@ import {IValidator} from "../src/IValidator.sol"; import {TypeCalculator} from "../src/types/TypeCalculator.sol"; import {ITypeCalculator} from "../src/types/ITypeCalculator.sol"; +import {SignedCommitHelper} from "./abstract/SignedCommitHelper.sol"; import {TestTeamRegistry} from "./mocks/TestTeamRegistry.sol"; /// Counts SLOAD / SSTORE access patterns on a warm steady-state turn, to ground the PLAN_OPT.md /// gas math in real data instead of estimates. -contract BatchInstrumentationTest is Test, EIP712 { +contract BatchInstrumentationTest is SignedCommitHelper { uint256 constant MONS_PER_TEAM = 4; uint256 constant MOVES_PER_MON = 4; @@ -47,10 +46,6 @@ contract BatchInstrumentationTest is Test, EIP712 { TestTeamRegistry defaultRegistry; StandardAttackFactory attackFactory; - function _domainNameAndVersion() internal pure override returns (string memory, string memory) { - return ("SignedCommitManager", "1"); - } - function setUp() public { p0 = vm.addr(P0_PK); p1 = vm.addr(P1_PK); @@ -103,84 +98,60 @@ contract BatchInstrumentationTest is Test, EIP712 { return battleKey; } - function _signDualReveal( - uint256 privateKey, - bytes32 battleKey, - uint64 turnId, - bytes32 committerMoveHash, - uint8 revealerMoveIndex, - bytes32 revealerSalt, - uint240 revealerExtraData - ) internal view returns (bytes memory) { - bytes32 domainSeparator = keccak256( - abi.encode( - _DOMAIN_TYPEHASH, - keccak256("SignedCommitManager"), - keccak256("1"), - block.chainid, - address(signedCommitManager) - ) - ); - bytes32 structHash = SignedCommitLib.hashDualSignedReveal( - SignedCommitLib.DualSignedReveal({ - battleKey: battleKey, - turnId: turnId, - committerMoveHash: committerMoveHash, - revealerMoveIndex: revealerMoveIndex, - revealerSalt: revealerSalt, - revealerExtraData: revealerExtraData - }) - ); - bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); - return abi.encodePacked(r, s, v); - } - function _fastTurn( bytes32 battleKey, uint8 p0MoveIndex, uint8 p1MoveIndex, - uint240 p0ExtraData, - uint240 p1ExtraData + uint16 p0ExtraData, + uint16 p1ExtraData ) internal { uint64 turnId = uint64(engine.getTurnIdForBattleState(battleKey)); - bytes32 committerSalt = keccak256(abi.encode("committer", battleKey, turnId)); - bytes32 revealerSalt = keccak256(abi.encode("revealer", battleKey, turnId)); + uint104 committerSalt = uint104(uint256(keccak256(abi.encode("committer", battleKey, turnId)))); + uint104 revealerSalt = uint104(uint256(keccak256(abi.encode("revealer", battleKey, turnId)))); uint8 committerMoveIndex; - uint240 committerExtraData; + uint16 committerExtraData; uint8 revealerMoveIndex; - uint240 revealerExtraData; + uint16 revealerExtraData; + uint256 committerPk; uint256 revealerPk; - address committer; if (turnId % 2 == 0) { committerMoveIndex = p0MoveIndex; committerExtraData = p0ExtraData; revealerMoveIndex = p1MoveIndex; revealerExtraData = p1ExtraData; + committerPk = P0_PK; revealerPk = P1_PK; - committer = p0; } else { committerMoveIndex = p1MoveIndex; committerExtraData = p1ExtraData; revealerMoveIndex = p0MoveIndex; revealerExtraData = p0ExtraData; + committerPk = P1_PK; revealerPk = P0_PK; - committer = p1; } bytes32 committerMoveHash = keccak256(abi.encodePacked(committerMoveIndex, committerSalt, committerExtraData)); + bytes memory committerSig = + _signCommit(address(signedCommitManager), committerPk, committerMoveHash, battleKey, turnId); bytes memory revealerSig = _signDualReveal( - revealerPk, battleKey, turnId, committerMoveHash, revealerMoveIndex, revealerSalt, revealerExtraData + address(signedCommitManager), + revealerPk, + battleKey, + turnId, + committerMoveHash, + revealerMoveIndex, + revealerSalt, + revealerExtraData ); - vm.prank(committer); signedCommitManager.executeWithDualSignedMoves( battleKey, committerMoveIndex, committerSalt, committerExtraData, revealerMoveIndex, revealerSalt, revealerExtraData, + committerSig, revealerSig ); engine.resetCallContext(); @@ -314,7 +285,7 @@ contract BatchInstrumentationTest is Test, EIP712 { vm.warp(vm.getBlockTimestamp() + 1); // Warm-up: lead-in switch + 1 damage trade. - _fastTurn(battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint240(0), uint240(0)); + _fastTurn(battleKey, SWITCH_MOVE_INDEX, SWITCH_MOVE_INDEX, uint16(0), uint16(0)); _fastTurn(battleKey, 0, 0, 0, 0); // Now profile a steady-state warm turn. diff --git a/test/EngineGlobalKVTest.sol b/test/EngineGlobalKVTest.sol index 7b1f7990..91a8cd5a 100644 --- a/test/EngineGlobalKVTest.sol +++ b/test/EngineGlobalKVTest.sol @@ -9,7 +9,6 @@ import "../src/Structs.sol"; import {DefaultCommitManager} from "../src/commit-manager/DefaultCommitManager.sol"; import {DefaultValidator} from "../src/DefaultValidator.sol"; import {Engine} from "../src/Engine.sol"; -import {MonStateIndexName, Type} from "../src/Enums.sol"; import {IEngine} from "../src/IEngine.sol"; import {DefaultMatchmaker} from "../src/matchmaker/DefaultMatchmaker.sol"; diff --git a/test/GachaTest.sol b/test/GachaTest.sol index f116e144..87abb7ae 100644 --- a/test/GachaTest.sol +++ b/test/GachaTest.sol @@ -10,7 +10,6 @@ import {DefaultValidator} from "../src/DefaultValidator.sol"; import {GachaRegistry} from "../src/gacha/GachaRegistry.sol"; import {DefaultRandomnessOracle} from "../src/rng/DefaultRandomnessOracle.sol"; -import {IGachaRNG} from "../src/rng/IGachaRNG.sol"; import {DefaultMonRegistry} from "../src/teams/DefaultMonRegistry.sol"; import {BattleHelper} from "./abstract/BattleHelper.sol"; diff --git a/test/InlineEngineGasTest.sol b/test/InlineEngineGasTest.sol index 19842928..5d61f5d3 100644 --- a/test/InlineEngineGasTest.sol +++ b/test/InlineEngineGasTest.sol @@ -10,7 +10,6 @@ import "../src/Structs.sol"; import {DefaultRuleset} from "../src/DefaultRuleset.sol"; import {DefaultCommitManager} from "../src/commit-manager/DefaultCommitManager.sol"; -import {SignedCommitLib} from "../src/commit-manager/SignedCommitLib.sol"; import {SignedCommitManager} from "../src/commit-manager/SignedCommitManager.sol"; import {Engine} from "../src/Engine.sol"; import {IEngine} from "../src/IEngine.sol"; diff --git a/test/SignedCommitManager.t.sol b/test/SignedCommitManager.t.sol index 43abefe3..843774e9 100644 --- a/test/SignedCommitManager.t.sol +++ b/test/SignedCommitManager.t.sol @@ -18,7 +18,6 @@ import {TestTeamRegistry} from "./mocks/TestTeamRegistry.sol"; import {DefaultMatchmaker} from "../src/matchmaker/DefaultMatchmaker.sol"; import {BattleHelper} from "./abstract/BattleHelper.sol"; import {SignedCommitHelper} from "./abstract/SignedCommitHelper.sol"; -import {SignedCommitLib} from "../src/commit-manager/SignedCommitLib.sol"; import {TestMoveFactory} from "./mocks/TestMoveFactory.sol"; abstract contract SignedCommitManagerTestBase is BattleHelper, SignedCommitHelper { diff --git a/test/StandardAttackPvPGasTest.sol b/test/StandardAttackPvPGasTest.sol index a0d2e1b0..cb1e91e9 100644 --- a/test/StandardAttackPvPGasTest.sol +++ b/test/StandardAttackPvPGasTest.sol @@ -9,7 +9,6 @@ import "../src/Structs.sol"; import {Engine} from "../src/Engine.sol"; import {SignedCommitManager} from "../src/commit-manager/SignedCommitManager.sol"; -import {SignedCommitLib} from "../src/commit-manager/SignedCommitLib.sol"; import {SignedMatchmaker} from "../src/matchmaker/SignedMatchmaker.sol"; import {BattleOfferLib} from "../src/matchmaker/BattleOfferLib.sol"; import {StandardAttackFactory} from "../src/moves/StandardAttackFactory.sol"; diff --git a/test/mons/PengymTest.sol b/test/mons/PengymTest.sol index 8c0e75c0..3ff3f6b5 100644 --- a/test/mons/PengymTest.sol +++ b/test/mons/PengymTest.sol @@ -187,7 +187,7 @@ contract PengymTest is Test, BattleHelper { (EffectInstance[] memory aliceEffects, ) = engine.getEffects(battleKey, 0, 0); bool hasPostWorkoutEffect = false; for (uint256 i = 0; i < aliceEffects.length; i++) { - if (aliceEffects[i].effect == IEffect(address(postWorkout))) { + if (address(aliceEffects[i].effect) == address(postWorkout)) { hasPostWorkoutEffect = true; break; } @@ -204,7 +204,7 @@ contract PengymTest is Test, BattleHelper { (aliceEffects, ) = engine.getEffects(battleKey, 0, 0); bool hasPanicEffect = false; for (uint256 i = 0; i < aliceEffects.length; i++) { - if (aliceEffects[i].effect == IEffect(address(panicStatus))) { + if (address(aliceEffects[i].effect) == address(panicStatus)) { hasPanicEffect = true; break; } @@ -229,7 +229,7 @@ contract PengymTest is Test, BattleHelper { hasPanicEffect = false; for (uint256 i = 0; i < aliceEffects.length; i++) { - if (aliceEffects[i].effect == IEffect(address(panicStatus))) { + if (address(aliceEffects[i].effect) == address(panicStatus)) { hasPanicEffect = true; break; } @@ -362,7 +362,7 @@ contract PengymTest is Test, BattleHelper { (EffectInstance[] memory aliceEffects, ) = engine.getEffects(battleKey, 0, 0); bool hasPostWorkoutEffect = false; for (uint256 i = 0; i < aliceEffects.length; i++) { - if (aliceEffects[i].effect == IEffect(address(postWorkout))) { + if (address(aliceEffects[i].effect) == address(postWorkout)) { hasPostWorkoutEffect = true; break; } @@ -379,7 +379,7 @@ contract PengymTest is Test, BattleHelper { (aliceEffects, ) = engine.getEffects(battleKey, 0, 0); bool hasFrostbiteEffect = false; for (uint256 i = 0; i < aliceEffects.length; i++) { - if (aliceEffects[i].effect == IEffect(address(frostbiteStatus))) { + if (address(aliceEffects[i].effect) == address(frostbiteStatus)) { hasFrostbiteEffect = true; break; } @@ -408,7 +408,7 @@ contract PengymTest is Test, BattleHelper { (aliceEffects, ) = engine.getEffects(battleKey, 0, 0); hasFrostbiteEffect = false; for (uint256 i = 0; i < aliceEffects.length; i++) { - if (aliceEffects[i].effect == IEffect(address(frostbiteStatus))) { + if (address(aliceEffects[i].effect) == address(frostbiteStatus)) { hasFrostbiteEffect = true; break; }