mirror of
https://github.com/luxfi/lamport.git
synced 2026-10-30 16:21:45 +00:00
LamportKeyChain now accepts max size at deploy: - constructor(uint256 _maxChainSize) with bounds [1, 1000] - Recommended: 256 keys (~5M gas, ~8 months at 1tx/day) - immutable for gas efficiency Practical gas limits documented: - 100 keys ≈ 2M gas - 256 keys ≈ 5M gas (recommended) - 512 keys ≈ 10M gas - 1000 keys ≈ 20M gas (near block limit)
349 lines
12 KiB
Solidity
349 lines
12 KiB
Solidity
// SPDX-License-Identifier: BSD-3-Clause
|
|
pragma solidity ^0.8.31;
|
|
|
|
import {Lamport} from "./Lamport.sol";
|
|
|
|
/// @title ISafe
|
|
/// @notice Minimal Safe interface
|
|
interface ISafe {
|
|
enum Operation {
|
|
Call,
|
|
DelegateCall
|
|
}
|
|
|
|
function execTransactionFromModule(
|
|
address to,
|
|
uint256 value,
|
|
bytes memory data,
|
|
Operation operation
|
|
) external returns (bool success);
|
|
|
|
function getTransactionHash(
|
|
address to,
|
|
uint256 value,
|
|
bytes calldata data,
|
|
uint8 operation,
|
|
uint256 safeTxGas,
|
|
uint256 baseGas,
|
|
uint256 gasPrice,
|
|
address gasToken,
|
|
address refundReceiver,
|
|
uint256 _nonce
|
|
) external view returns (bytes32);
|
|
}
|
|
|
|
/// @title LamportThreshold
|
|
/// @notice Threshold Lamport signatures for Safe multisig
|
|
/// @author Lux Network Team
|
|
/// @dev T-Chain MPC controls ONE Lamport key off-chain. On-chain sees standard signature.
|
|
///
|
|
/// SECURITY MODEL:
|
|
/// - Threshold (t-of-n) enforced by T-Chain MPC network
|
|
/// - On-chain sees ONE standard Lamport signature
|
|
/// - Domain separation prevents replay attacks
|
|
/// - Atomic key rotation after each signature
|
|
///
|
|
/// See: LP-4105 (Lamport OTS for Lux Safe)
|
|
contract LamportThreshold {
|
|
// =========================================================================
|
|
// State
|
|
// =========================================================================
|
|
|
|
address public immutable safe;
|
|
bytes32 public pkh;
|
|
bool public initialized;
|
|
uint256 public nonce;
|
|
|
|
// =========================================================================
|
|
// Events
|
|
// =========================================================================
|
|
|
|
event Initialized(bytes32 indexed pkh);
|
|
event KeyRotated(bytes32 indexed oldPkh, bytes32 indexed newPkh);
|
|
event Executed(bytes32 indexed txHash, bytes32 indexed nextPkh, uint256 nonce);
|
|
|
|
// =========================================================================
|
|
// Errors
|
|
// =========================================================================
|
|
|
|
error NotInitialized();
|
|
error AlreadyInitialized();
|
|
error OnlySafe();
|
|
error InvalidPKH();
|
|
error InvalidSignature();
|
|
|
|
// =========================================================================
|
|
// Modifiers
|
|
// =========================================================================
|
|
|
|
modifier onlySafe() {
|
|
if (msg.sender != safe) revert OnlySafe();
|
|
_;
|
|
}
|
|
|
|
// =========================================================================
|
|
// Constructor
|
|
// =========================================================================
|
|
|
|
constructor(address _safe) {
|
|
require(_safe != address(0), "zero address");
|
|
safe = _safe;
|
|
}
|
|
|
|
// =========================================================================
|
|
// Initialization
|
|
// =========================================================================
|
|
|
|
/// @notice Initialize with first PKH
|
|
/// @param firstPKH Hash of initial public key
|
|
function init(bytes32 firstPKH) external onlySafe {
|
|
if (initialized) revert AlreadyInitialized();
|
|
require(firstPKH != bytes32(0), "zero PKH");
|
|
pkh = firstPKH;
|
|
initialized = true;
|
|
emit Initialized(firstPKH);
|
|
}
|
|
|
|
// =========================================================================
|
|
// Execution
|
|
// =========================================================================
|
|
|
|
/// @notice Execute Safe transaction with threshold Lamport signature
|
|
/// @param to Destination address
|
|
/// @param value ETH value
|
|
/// @param data Call data
|
|
/// @param operation 0=Call, 1=DelegateCall
|
|
/// @param sig Lamport signature (256 preimages)
|
|
/// @param currentPub Current public key
|
|
/// @param nextPKH Next public key hash (for rotation)
|
|
function exec(
|
|
address to,
|
|
uint256 value,
|
|
bytes calldata data,
|
|
uint8 operation,
|
|
bytes32[256] calldata sig,
|
|
bytes32[2][256] calldata currentPub,
|
|
bytes32 nextPKH
|
|
) external returns (bool success) {
|
|
if (!initialized) revert NotInitialized();
|
|
|
|
// 1. Verify current public key matches stored PKH
|
|
bytes32 actualPKH = Lamport.computePKHCalldata(currentPub);
|
|
if (actualPKH != pkh) revert InvalidPKH();
|
|
|
|
// 2. Compute safeTxHash ON-CHAIN (SECURITY CRITICAL)
|
|
bytes32 safeTxHash = ISafe(safe)
|
|
.getTransactionHash(to, value, data, operation, 0, 0, 0, address(0), payable(0), nonce);
|
|
|
|
// 3. Domain-separated message (prevents replay)
|
|
uint256 m = Lamport.computeMessage(safeTxHash, nextPKH, address(this), block.chainid);
|
|
|
|
// 4. Verify Lamport signature
|
|
if (!Lamport.verify(m, sig, currentPub)) {
|
|
revert InvalidSignature();
|
|
}
|
|
|
|
// 5. Rotate to next key
|
|
bytes32 oldPkh = pkh;
|
|
pkh = nextPKH;
|
|
nonce++;
|
|
|
|
emit KeyRotated(oldPkh, nextPKH);
|
|
emit Executed(safeTxHash, nextPKH, nonce - 1);
|
|
|
|
// 6. Execute via Safe
|
|
success = ISafe(safe).execTransactionFromModule(to, value, data, ISafe.Operation(operation));
|
|
}
|
|
|
|
/// @notice Convenience function for Call operation
|
|
function execCall(
|
|
address to,
|
|
uint256 value,
|
|
bytes calldata data,
|
|
bytes32[256] calldata sig,
|
|
bytes32[2][256] calldata currentPub,
|
|
bytes32 nextPKH
|
|
) external returns (bool success) {
|
|
return this.exec(to, value, data, 0, sig, currentPub, nextPKH);
|
|
}
|
|
|
|
// =========================================================================
|
|
// View Functions
|
|
// =========================================================================
|
|
|
|
/// @notice Compute message hash for off-chain signing
|
|
function computeMessageHash(
|
|
address to,
|
|
uint256 value,
|
|
bytes calldata data,
|
|
uint8 operation,
|
|
bytes32 nextPKH
|
|
) external view returns (uint256 m) {
|
|
bytes32 safeTxHash = ISafe(safe)
|
|
.getTransactionHash(to, value, data, operation, 0, 0, 0, address(0), payable(0), nonce);
|
|
m = Lamport.computeMessage(safeTxHash, nextPKH, address(this), block.chainid);
|
|
}
|
|
|
|
/// @notice Get current PKH
|
|
function getPKH() external view returns (bytes32) {
|
|
return pkh;
|
|
}
|
|
|
|
/// @notice Check if initialized
|
|
function isInitialized() external view returns (bool) {
|
|
return initialized;
|
|
}
|
|
|
|
/// @notice Get current nonce
|
|
function getNonce() external view returns (uint256) {
|
|
return nonce;
|
|
}
|
|
}
|
|
|
|
/// @title LamportKeyChain
|
|
/// @notice Registry for managing pre-generated Lamport key chains
|
|
/// @dev For threshold setups, keys are pre-generated and registered
|
|
contract LamportKeyChain {
|
|
// =========================================================================
|
|
// Configuration
|
|
// =========================================================================
|
|
|
|
/// @notice Maximum keys per chain (prevents gas griefing)
|
|
/// @dev Configurable at deploy. Practical limits:
|
|
/// - 100 keys ≈ 2M gas
|
|
/// - 256 keys ≈ 5M gas (recommended default)
|
|
/// - 512 keys ≈ 10M gas
|
|
/// - 1000 keys ≈ 20M gas (near block limit)
|
|
uint256 public immutable maxChainSize;
|
|
|
|
// =========================================================================
|
|
// Types
|
|
// =========================================================================
|
|
|
|
struct KeyChain {
|
|
bytes32[] pkhs;
|
|
uint256 currentIndex;
|
|
address owner;
|
|
bool active;
|
|
}
|
|
|
|
// =========================================================================
|
|
// State
|
|
// =========================================================================
|
|
|
|
mapping(bytes32 => KeyChain) public keyChains;
|
|
mapping(address => bytes32[]) public ownerChains;
|
|
mapping(bytes32 => bool) public usedPKHs;
|
|
uint256 public chainCount;
|
|
|
|
// =========================================================================
|
|
// Events
|
|
// =========================================================================
|
|
|
|
event ChainRegistered(bytes32 indexed chainId, address indexed owner, uint256 keyCount);
|
|
event KeyUsed(bytes32 indexed chainId, bytes32 indexed pkh, uint256 index);
|
|
event ChainDeactivated(bytes32 indexed chainId);
|
|
|
|
// =========================================================================
|
|
// Errors
|
|
// =========================================================================
|
|
|
|
error ChainNotFound();
|
|
error NotChainOwner();
|
|
error ChainNotActive();
|
|
error ChainExhausted();
|
|
error PKHAlreadyUsed();
|
|
error InvalidPKH();
|
|
error EmptyKeyArray();
|
|
error ChainTooLarge();
|
|
|
|
// =========================================================================
|
|
// Constructor
|
|
// =========================================================================
|
|
|
|
/// @notice Deploy with configurable max chain size
|
|
/// @param _maxChainSize Maximum keys per chain (recommend 256)
|
|
constructor(uint256 _maxChainSize) {
|
|
require(_maxChainSize > 0 && _maxChainSize <= 1000, "Invalid max size");
|
|
maxChainSize = _maxChainSize;
|
|
}
|
|
|
|
// =========================================================================
|
|
// Registration
|
|
// =========================================================================
|
|
|
|
/// @notice Register a new key chain
|
|
function registerKeyChain(bytes32[] calldata pkhs) external returns (bytes32 chainId) {
|
|
if (pkhs.length == 0) revert EmptyKeyArray();
|
|
if (pkhs.length > maxChainSize) revert ChainTooLarge();
|
|
|
|
for (uint256 i = 0; i < pkhs.length; i++) {
|
|
if (pkhs[i] == bytes32(0)) revert InvalidPKH();
|
|
if (usedPKHs[pkhs[i]]) revert PKHAlreadyUsed();
|
|
}
|
|
|
|
chainId = keccak256(abi.encodePacked(msg.sender, chainCount++, block.timestamp));
|
|
|
|
KeyChain storage chain = keyChains[chainId];
|
|
chain.pkhs = pkhs;
|
|
chain.currentIndex = 0;
|
|
chain.owner = msg.sender;
|
|
chain.active = true;
|
|
|
|
ownerChains[msg.sender].push(chainId);
|
|
emit ChainRegistered(chainId, msg.sender, pkhs.length);
|
|
}
|
|
|
|
/// @notice Get current PKH for chain
|
|
function getCurrentPKH(bytes32 chainId) external view returns (bytes32) {
|
|
KeyChain storage chain = keyChains[chainId];
|
|
if (!chain.active) revert ChainNotActive();
|
|
if (chain.currentIndex >= chain.pkhs.length) revert ChainExhausted();
|
|
return chain.pkhs[chain.currentIndex];
|
|
}
|
|
|
|
/// @notice Get next PKH for rotation commitment
|
|
function getNextPKH(bytes32 chainId) external view returns (bytes32) {
|
|
KeyChain storage chain = keyChains[chainId];
|
|
if (!chain.active) revert ChainNotActive();
|
|
|
|
uint256 nextIndex = chain.currentIndex + 1;
|
|
if (nextIndex >= chain.pkhs.length) return bytes32(0);
|
|
return chain.pkhs[nextIndex];
|
|
}
|
|
|
|
/// @notice Mark current key as used and advance
|
|
function advanceKey(bytes32 chainId) external {
|
|
KeyChain storage chain = keyChains[chainId];
|
|
if (chain.owner != msg.sender) revert NotChainOwner();
|
|
if (!chain.active) revert ChainNotActive();
|
|
if (chain.currentIndex >= chain.pkhs.length) revert ChainExhausted();
|
|
|
|
bytes32 usedPKH = chain.pkhs[chain.currentIndex];
|
|
usedPKHs[usedPKH] = true;
|
|
|
|
emit KeyUsed(chainId, usedPKH, chain.currentIndex);
|
|
chain.currentIndex++;
|
|
}
|
|
|
|
/// @notice Get remaining keys
|
|
function getRemainingKeys(bytes32 chainId) external view returns (uint256) {
|
|
KeyChain storage chain = keyChains[chainId];
|
|
if (chain.currentIndex >= chain.pkhs.length) return 0;
|
|
return chain.pkhs.length - chain.currentIndex;
|
|
}
|
|
|
|
/// @notice Deactivate chain
|
|
function deactivateChain(bytes32 chainId) external {
|
|
KeyChain storage chain = keyChains[chainId];
|
|
if (chain.owner != msg.sender) revert NotChainOwner();
|
|
chain.active = false;
|
|
emit ChainDeactivated(chainId);
|
|
}
|
|
|
|
/// @notice Get owner's chains
|
|
function getOwnerChains(address owner) external view returns (bytes32[] memory) {
|
|
return ownerChains[owner];
|
|
}
|
|
}
|