@@ -7,6 +7,7 @@ import {ECDSA} from "../lib/ECDSA.sol";
77import {SignedCommitLib} from "./SignedCommitLib.sol " ;
88import {IEngine} from "../IEngine.sol " ;
99import {CommitContext, PlayerDecisionData} from "../Structs.sol " ;
10+ import {GameMode} from "../Enums.sol " ;
1011
1112/// @title SignedCommitManager
1213/// @notice Extends DefaultCommitManager with optimistic dual-signed commit flow
@@ -41,6 +42,9 @@ contract SignedCommitManager is DefaultCommitManager, EIP712 {
4142 /// @notice Thrown when trying to use dual-signed flow on a single-player turn
4243 error NotTwoPlayerTurn ();
4344
45+ /// @notice Thrown when using singles function for a doubles battle or vice versa
46+ error WrongGameMode ();
47+
4448 constructor (IEngine engine ) DefaultCommitManager (engine) {}
4549
4650 /// @inheritdoc EIP712
@@ -54,7 +58,7 @@ contract SignedCommitManager is DefaultCommitManager, EIP712 {
5458 version = "1 " ;
5559 }
5660
57- /// @notice Executes a turn using dual-signed moves from both players (gas-optimized )
61+ /// @notice Executes a turn using dual-signed moves from both players (singles only )
5862 /// @dev The committer (A) submits both moves. The revealer (B) has signed over
5963 /// their move and A's move hash, binding both players to their moves.
6064 /// @param battleKey The battle identifier
@@ -77,9 +81,11 @@ contract SignedCommitManager is DefaultCommitManager, EIP712 {
7781 bytes memory revealerSignature
7882 ) external {
7983 // Use lightweight getter (validates internally, reverts on bad state)
80- (address committer , address revealer , uint64 turnId ) =
84+ (address committer , address revealer , uint64 turnId , GameMode gameMode ) =
8185 ENGINE.getCommitAuthForDualSigned (battleKey);
8286
87+ if (gameMode != GameMode.Singles) revert WrongGameMode ();
88+
8389 // Caller must be the committing player
8490 if (msg .sender != committer) {
8591 revert CallerNotCommitter ();
@@ -123,10 +129,102 @@ contract SignedCommitManager is DefaultCommitManager, EIP712 {
123129 }
124130 }
125131
132+ /// @notice Executes a turn using dual-signed moves for doubles battles
133+ /// @dev Same security model as executeWithDualSignedMoves but each player has 2 slot moves.
134+ /// The committer's hash covers both slot moves: keccak256(moveIndex0, moveIndex1, salt, extraData0, extraData1)
135+ /// matching the revealMovePair preimage format.
136+ /// @param battleKey The battle identifier
137+ /// @param committerMoveIndex0 The committer's slot 0 move index
138+ /// @param committerExtraData0 The committer's slot 0 extra data
139+ /// @param committerMoveIndex1 The committer's slot 1 move index
140+ /// @param committerExtraData1 The committer's slot 1 extra data
141+ /// @param committerSalt The committer's salt (shared across both slots)
142+ /// @param revealerMoveIndex0 The revealer's slot 0 move index
143+ /// @param revealerExtraData0 The revealer's slot 0 extra data
144+ /// @param revealerMoveIndex1 The revealer's slot 1 move index
145+ /// @param revealerExtraData1 The revealer's slot 1 extra data
146+ /// @param revealerSalt The revealer's salt (shared across both slots)
147+ /// @param revealerSignature EIP-712 signature from the revealer over DualSignedRevealDoubles
148+ function executeWithDualSignedMovesForDoubles (
149+ bytes32 battleKey ,
150+ uint8 committerMoveIndex0 ,
151+ uint240 committerExtraData0 ,
152+ uint8 committerMoveIndex1 ,
153+ uint240 committerExtraData1 ,
154+ bytes32 committerSalt ,
155+ uint8 revealerMoveIndex0 ,
156+ uint240 revealerExtraData0 ,
157+ uint8 revealerMoveIndex1 ,
158+ uint240 revealerExtraData1 ,
159+ bytes32 revealerSalt ,
160+ bytes memory revealerSignature
161+ ) external {
162+ // Use lightweight getter (validates internally, reverts on bad state)
163+ (address committer , address revealer , uint64 turnId , GameMode gameMode ) =
164+ ENGINE.getCommitAuthForDualSigned (battleKey);
165+
166+ if (gameMode != GameMode.Doubles) revert WrongGameMode ();
167+
168+ // Caller must be the committing player
169+ if (msg .sender != committer) {
170+ revert CallerNotCommitter ();
171+ }
172+
173+ // Compute the committer's move hash (matches revealMovePair preimage format)
174+ bytes32 committerMoveHash = keccak256 (
175+ abi.encodePacked (
176+ committerMoveIndex0, committerMoveIndex1, committerSalt, committerExtraData0, committerExtraData1
177+ )
178+ );
179+
180+ // Verify the revealer's signature over DualSignedRevealDoubles
181+ SignedCommitLib.DualSignedRevealDoubles memory reveal = SignedCommitLib.DualSignedRevealDoubles ({
182+ battleKey: battleKey,
183+ turnId: turnId,
184+ committerMoveHash: committerMoveHash,
185+ revealerMoveIndex0: revealerMoveIndex0,
186+ revealerMoveIndex1: revealerMoveIndex1,
187+ revealerSalt: revealerSalt,
188+ revealerExtraData0: revealerExtraData0,
189+ revealerExtraData1: revealerExtraData1
190+ });
191+
192+ bytes32 digest = _hashTypedData (SignedCommitLib.hashDualSignedRevealDoubles (reveal));
193+ if (ECDSA.recover (digest, revealerSignature) != revealer) {
194+ revert InvalidSignature ();
195+ }
196+
197+ // Execute with all 4 slot moves in a single call
198+ if (turnId % 2 == 0 ) {
199+ // Committer is p0
200+ ENGINE.executeWithMovesForDoubles (
201+ battleKey,
202+ committerMoveIndex0, committerExtraData0,
203+ committerMoveIndex1, committerExtraData1,
204+ committerSalt,
205+ revealerMoveIndex0, revealerExtraData0,
206+ revealerMoveIndex1, revealerExtraData1,
207+ revealerSalt
208+ );
209+ } else {
210+ // Committer is p1
211+ ENGINE.executeWithMovesForDoubles (
212+ battleKey,
213+ revealerMoveIndex0, revealerExtraData0,
214+ revealerMoveIndex1, revealerExtraData1,
215+ revealerSalt,
216+ committerMoveIndex0, committerExtraData0,
217+ committerMoveIndex1, committerExtraData1,
218+ committerSalt
219+ );
220+ }
221+ }
222+
126223 /// @notice Allows anyone to publish the committer's signed commitment on-chain
127224 /// @dev This is a fallback mechanism if the committer (A) doesn't submit via
128225 /// executeWithDualSignedMoves. The revealer (B) can use this to force A's
129226 /// commitment on-chain, then proceed with the normal reveal flow.
227+ /// Works for both singles and doubles - the moveHash is format-agnostic.
130228 /// @param battleKey The battle identifier
131229 /// @param moveHash The committer's move hash
132230 /// @param committerSignature EIP-712 signature from the committer over
0 commit comments