## 介绍 在智能合约开发中,当需要验证签名时,通常会有nonce字段,用于防止签名重放攻击。而我们常常会直接设计成nonce从小到大,按顺序增长。然而在高频交易的场景下,这样会存在着缺陷,而另一个更好实现是 —— 无序nonce。 <br> ## 顺序nonce 示例: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract TraditionalNonceExample { // 每个地址的当前 nonce(从 0 开始递增) mapping(address => uint256) public nonceOf; error InvalidNonce(uint256 expected, uint256 provided); // 模拟一个需要签名授权的操作(比如 permit) function doSomethingImportant( address user, uint256 amount, uint256 deadline, uint256 providedNonce, // 用户在签名里填的 nonce uint8 v, bytes32 r, bytes32 s ) external { require(block.timestamp <= deadline, "Expired"); // 关键:检查 nonce 必须等于当前期望值(顺序递增),并且+1 uint256 expectedNonce = nonceOf[user]++; if (providedNonce != expectedNonce) { revert InvalidNonce(expectedNonce, providedNonce); } // 验证签名 bytes32 messageHash = keccak256( abi.encode( keccak256("DoSomething(address user,uint256 amount,uint256 deadline,uint256 nonce)"), // EIP-712 type hash user, amount, deadline, providedNonce ) ); bytes32 ethSignedMessageHash = keccak256(abi.encodePacked("\x19\x01", block.chainid, address(this), messageHash)); address signer = ecrecover(ethSignedMessageHash, v, r, s); require(signer == user, "Invalid signature"); // 执行你的业务逻辑(比如转账、swap 等) // _doYourThing(user, amount); } // 让用户可以主动跳过一些坏掉的 nonce function advanceNonce(uint256 amountToSkip) external { nonceOf[msg.sender] += amountToSkip; } } ``` 假设该nonce模块是应用在高频交易上,同一时刻提交了20笔交易,对应着0 ~ 19 nonce。那么这20笔交易必须按nonce的顺序去执行,如果nonce 1的交易排在nonce 0的交易前面,则nonce 1的交易会报错无法执行。更严重的,nonce 1交易的失败,也会导致后面nonce的交易失败。 而为了应对这种情况,出现了无序nonce的机制。 <br> ## 无序nonce 这里参考Uniswap的Permit2合约中的无序nonce实现来讲解: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract TraditionalNonceExample { mapping(address user => mapping(uint256 wordPos => uint256)) public nonceBitmap; error InvalidNonce(); // 模拟一个需要签名授权的操作(比如 permit) function doSomethingImportant( address user, uint256 amount, uint256 deadline, uint256 providedNonce, // 用户在签名里填的 nonce uint8 v, bytes32 r, bytes32 s ) external { require(block.timestamp <= deadline, "Expired"); // 关键:检查 nonce _useUnorderedNonce(user, providedNonce); // 验证签名 bytes32 messageHash = keccak256( abi.encode( keccak256("DoSomething(address user,uint256 amount,uint256 deadline,uint256 nonce)"), // EIP-712 type hash user, amount, deadline, providedNonce ) ); bytes32 ethSignedMessageHash = keccak256(abi.encodePacked("\x19\x01", block.chainid, address(this), messageHash)); address signer = ecrecover(ethSignedMessageHash, v, r, s); require(signer == user, "Invalid signature"); // 执行你的业务逻辑(比如转账、swap 等) // _doYourThing(user, amount); } /// ============ Uniswap Permit2 unordered nonce ============ /// @inheritdoc ISignatureTransfer function invalidateUnorderedNonces(uint256 wordPos, uint256 mask) external { nonceBitmap[msg.sender][wordPos] |= mask; emit UnorderedNonceInvalidation(msg.sender, wordPos, mask); } /// @notice Returns the index of the bitmap and the bit position within the bitmap. Used for unordered nonces /// @param nonce The nonce to get the associated word and bit positions /// @return wordPos The word position or index into the nonceBitmap /// @return bitPos The bit position /// @dev The first 248 bits of the nonce value is the index of the desired bitmap /// @dev The last 8 bits of the nonce value is the position of the bit in the bitmap function bitmapPositions(uint256 nonce) private pure returns (uint256 wordPos, uint256 bitPos) { wordPos = uint248(nonce >> 8); bitPos = uint8(nonce); } /// @notice Checks whether a nonce is taken and sets the bit at the bit position in the bitmap at the word position /// @param from The address to use the nonce at /// @param nonce The nonce to spend function _useUnorderedNonce(address from, uint256 nonce) internal { (uint256 wordPos, uint256 bitPos) = bitmapPositions(nonce); uint256 bit = 1 << bitPos; uint256 flipped = nonceBitmap[from][wordPos] ^= bit; if (flipped & bit == 0) revert InvalidNonce(); } } ``` 在上述无序nonce的实现中,nonce的存储数据结构为:`mapping(address user => mapping(uint256 wordPos => uint256 bitPos)) public nonceBitmap;` 这里的wordPos实际上是个248bit的值,容量为 2^248。而每个wordPos会对应一个256大小的bitmap,这也就是代表着实际的nonce。 可以想象为:每个用户都有2^248个抽屉,而每个抽屉中都有256个位置,每个位置代表着一个nonce。位置原本是空的(bit值为0),当该nonce被使用后,位置会被填充(bit值为1)。 <br> 1. 在调用需要验证签名的函数时,会传一个准备使用的nonce,数据类型为uint256。该nonce值中包含着wordPos和bitPos数据,可通过`bitmapPositions()`函数进行解析。 ![image](https://hackmd.io/_uploads/SJc5oS3l-l.png) 2.在`_useUnorderedNonce()`函数中,会对wordPos和bitPos进行验证,nonce是否已经使用了。使用过了则报错会滚,没使用过,则设该nonce为已被使用。 ![image](https://hackmd.io/_uploads/HyS0sr2eWl.png) 3. 而`invalidateUnorderedNonces(uint256 wordPos, uint256 mask)`函数可以将某wordPos中某些未使用的nonce设为已使用。因此在实际使用时,如果某些交易可能需要同时取消,那么这些交易最好用同一个wordPos中的nonce,这样在取消时会更加方便。