diff --git a/docs/erc-7824.md b/docs/erc-7824.md index 2a6ae42..501bc8b 100644 --- a/docs/erc-7824.md +++ b/docs/erc-7824.md @@ -49,12 +49,6 @@ The Nitrolite protocol defines the following core data structures: #### Basic Types ```solidity -struct Signature { - uint8 v; - bytes32 r; - bytes32 s; -} - struct Amount { address token; // ERC-20 token address (address(0) for native tokens) uint256 amount; // Token amount @@ -86,7 +80,7 @@ struct State { uint256 version; // State version incremental number to compare most recent bytes data; // Application data encoded, decoded by the adjudicator Allocation[] allocations; // Asset allocation and destination for each participant - Signature[] sigs; // stateHash signatures from participants + bytes[] sigs; // stateHash signatures from participants } enum StateIntent { @@ -132,14 +126,15 @@ For signature verification, the state hash is computed as: bytes32 stateHash = keccak256( abi.encode( channelId, - state.data, + state.intent, state.version, + state.data, state.allocations ) ); ``` -Note: The stateHash is bare signed without EIP-191 since the protocol is intended to be chain-agnostic. +Note: The smart contract supports all popular signature formats, specifically: raw ECDSA, EIP-191, EIP-712, EIP-1271, and EIP-6492. ### Interfaces @@ -218,7 +213,7 @@ interface IChannel { * @param sig Signature of the participant on the funding state * @return channelId Unique identifier for the joined channel */ - function join(bytes32 channelId, uint256 index, Signature calldata sig) + function join(bytes32 channelId, uint256 index, bytes calldata sig) external returns (bytes32); /** @@ -253,11 +248,13 @@ interface IChannel { * @param channelId Unique identifier for the channel * @param candidate The state being submitted as the latest valid state * @param proofs Additional states required by the adjudicator + * @param challengerSig Signature of the challenger on the candidate state. Must be signed by one of the participants */ function challenge( bytes32 channelId, State calldata candidate, - State[] calldata proofs + State[] calldata proofs, + bytes calldata challengerSig ) external; /** @@ -284,25 +281,27 @@ interface IDeposit { /** * @notice Deposits tokens into the contract * @dev For native tokens, the value should be sent with the transaction + * @param wallet Address of the account whose ledger is changed * @param token Token address (use address(0) for native tokens) * @param amount Amount of tokens to deposit */ - function deposit(address token, uint256 amount) external payable; + function deposit(address wallet, address token, uint256 amount) external payable; /** * @notice Withdraws tokens from the contract * @dev Can only withdraw available (not locked in channels) funds + * @param wallet Address of the account whose ledger is changed * @param token Token address (use address(0) for native tokens) * @param amount Amount of tokens to withdraw */ - function withdraw(address token, uint256 amount) external; + function withdraw(address wallet, address token, uint256 amount) external; } ``` ### Channel Lifecycle -1. **Creation**: Creator constructs channel config and signs initial state with `StateIntent.INITIALIZE` -2. **Joining**: Participants verify the channel and sign the same funding state +1. **Creation**: Creator constructs channel config and signs initial state with `StateIntent.INITIALIZE`. Second participant are able to join a channel immediately by providing a signature over initial state, and funds will be deducted from their account, if available. +2. **Joining**: Participants verify the channel and sign the same funding state. This step can be omitted by providing a signature over the initial state when creating the channel. Note, however, that this means that funds will be locked from the participant's balance, while `join(...)` allows to fund the channel from external account. 3. **Active**: Once fully funded, the channel transitions to active state for off-chain operation 4. **Off-chain Updates**: Participants exchange and sign state updates according to application logic 5. **Resolution**: @@ -456,7 +455,7 @@ No backward compatibility issues found. This ERC is designed to coexist with exi ### On-Chain Security -- **Signature Verification**: All state transitions require valid signatures from participants. The protocol uses bare signatures (without EIP-191) for chain agnosticism. +- **Signature Verification**: All state transitions require valid signatures from participants. The protocol supports signatures of all popular formats, including EIP-191 and EIP-712. - **Challenge Period**: The configurable challenge duration provides time for honest participants to respond to invalid states. - **Adjudicator Validation**: Custom adjudicators must be carefully audited as they control state transition rules. - **Reentrancy Protection**: Implementation should follow checks-effects-interactions pattern, especially in fund distribution. diff --git a/docs/nitrolite_client/advanced/supported-sig-formats.md b/docs/nitrolite_client/advanced/supported-sig-formats.md new file mode 100644 index 0000000..9422f13 --- /dev/null +++ b/docs/nitrolite_client/advanced/supported-sig-formats.md @@ -0,0 +1,86 @@ +--- +sidebar_position: 4 +title: Supported Signature Formats +description: Documentation for supported signature formats in NitroliteClient +keywords: [erc7824, statechannels, state channels, nitrolite, ethereum scaling, layer 2, off-chain, advanced, signature, format, ECDSA, EIP-191, EIP-712, EIP-1271, EIP-6492] +--- + +# Supported Signature Formats + +The nitrolite smart contract supports multiple signature formats over a State to accommodate various use cases and compatibility with different wallets and applications. + +The message being signed is a channelId and State, formatted in a specific way. The most common is a `packedState`, which is calculated as follows: + +```solidity +abi.encode(channelId, state.intent, state.version, state.data, state.allocations) +``` + +## EOA signatures + +Externally Owned Accounts (EOAs) can sign messages with their private key using the ECDSA. + +Based on how the message is handled before signing, the following formats are supported: + +### Raw ECDSA Signature + +The message is a `packedState`, that is hashed with `keccak256` before signing. The signature is a 65-byte ECDSA signature. + +### EIP-191 Signature + +You can read more about EIP-191 in the [EIP-191 specification](https://eips.ethereum.org/EIPS/eip-191). + +The message is a `packedState` prefixed with `"\x19Ethereum Signed Message:\n" + len(packedState)` and hashed with `keccak256` before signing. The signature is a 65-byte ECDSA signature. + +### EIP-712 Signature + +You can read more about EIP-712 in the [EIP-712 specification](https://eips.ethereum.org/EIPS/eip-712). + +The message is an `AllowStateHash` typed data, calculated as follows: + +```solidity +abi.encode( + typeHash, + channelId, + state.intent, + state.version, + keccak256(state.data), + keccak256(abi.encode(state.allocations)) +); +``` + +Where `typeHash` is `AllowStateHash(bytes32 channelId,uint8 intent,uint256 version,bytes data,Allocation[] allocations)Allocation(address destination,address token,uint256 amount)`. + +The message is then hashed with `keccak256`, appended to `"\x19\x01" || domainSeparator` and signed. The signature is a 65-byte ECDSA signature. + +`||` is a concatenation operator, and `domainSeparator` is calculated as follows: + +```solidity +keccak256( + abi.encode( + EIP712_TYPE_HASH, + keccak256(name), + keccak256(version), + chainId, + verifyingContract + ) +); +``` + +`EIP712_TYPE_HASH` is `keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")`. + +Additionally, `name`, `version` are the name and version of the Custody contract, `chainId` is the chain ID of the network, and `verifyingContract` is the address of the contract. + +## Smart Contract Signatures + +Smart Contracts that support [EIP-1271](https://eips.ethereum.org/EIPS/eip-1271) or [EIP-6492](https://eips.ethereum.org/EIPS/eip-6492) can sign messages using their own logic. When checking such signatures, the nitrolite smart contract will pass the `keccak256` hash of the `packedState` as a message hash for verification. + +See the aforementioned EIP standards for details on how these signatures are structured and verified. If you want to add support for such signatures in your client, you probably need to look at how signature verification logic is implemented in the Smart Contract (Smart Wallet, etc) that will use them. + +## Challenge Signatures + +The aforementioned signature formats are used to sign States, however to submit a challenge, the user must provide a `challengerSignature`, which proves that the user has the right to challenge a Channel. + +Depending on a signature format, the `challengerSignature` is calculated differently from the common State signature: + +- **Raw ECDSA, EIP-191, EIP-1271 and EIP-6492**: The message (`packedState`) is suffixed with a `challenge` string (`abi.encodePacked(packedState, "challenge")`). +- **EIP-712**: The `typeHash` name is `AllowChallengeStateHash`, while type format remains the same. diff --git a/docs/nitrolite_client/types.md b/docs/nitrolite_client/types.md index 607f5e0..74baa04 100644 --- a/docs/nitrolite_client/types.md +++ b/docs/nitrolite_client/types.md @@ -30,14 +30,10 @@ A hash of a channel state, represented as a hexadecimal string. ### Signature ```typescript -interface Signature { - v: number; // Recovery value - r: Hex; // First 32 bytes of the signature - s: Hex; // Second 32 bytes of the signature -} +type Signature = Hex; ``` -Represents a cryptographic signature used for signing state channel states. +Represents a cryptographic signature used for signing state channel states as a hexadecimal string. ### Allocation @@ -85,7 +81,7 @@ interface State { version: bigint; // Version number, incremented for each update data: Hex; // Application data encoded as hex allocations: [Allocation, Allocation]; // Asset allocation for each participant - sigs: Signature[]; // State hash signatures + sigs: Signature[]; // State hash signatures } ``` diff --git a/docs/quick_start/connect_to_the_clearnode.md b/docs/quick_start/connect_to_the_clearnode.md index 4a410ff..390866c 100644 --- a/docs/quick_start/connect_to_the_clearnode.md +++ b/docs/quick_start/connect_to_the_clearnode.md @@ -554,7 +554,7 @@ const messageSigner = async (payload: RequestData | ResponsePayload): Promise