Single Payment Signature Structure
Payment signatures are verified by Core.sol using validateAndConsumeNonce().
To authorize payment operations, the user must generate a cryptographic signature compliant with the EIP-191 standard using the Ethereum Signed Message format.
The signature verification is centralized in Core.sol, which validates the signature, checks nonce validity, and handles executor authorization in a single atomic operation.
Signature Format
{evvmId},{serviceAddress},{hashPayload},{executor},{nonce},{isAsyncExec}
Components:
- evvmId: Network identifier (uint256, typically
1) - serviceAddress: Core.sol contract address
- hashPayload: Hash of payment parameters (bytes32, from CoreHashUtils)
- executor: Address authorized to execute (address,
0x0...0for unrestricted) - nonce: User's centralized nonce from Core.sol (uint256)
- isAsyncExec: Execution mode -
truefor async,falsefor sync (boolean)
Hash Payload Generation
The hashPayload is generated using CoreHashUtils.hashDataForPay():
import {CoreHashUtils} from "@evvm/testnet-contracts/library/signature/CoreHashUtils.sol";
bytes32 hashPayload = CoreHashUtils.hashDataForPay(
receiver, // address or username (bytes32 hash)
token, // ERC20 token address (0x0...0 for ETH)
amount, // Amount in wei
priorityFee // Fee amount in wei
);
Hash Generation Process
CoreHashUtils creates a deterministic hash:
// Internal implementation (simplified)
function hashDataForPay(
bytes32 receiver,
address token,
uint256 amount,
uint256 priorityFee
) internal pure returns (bytes32) {
return keccak256(
abi.encodePacked(receiver, token, amount, priorityFee)
);
}
Key Points:
receiveris bytes32 (use0x0...0prefix for address, or username hash)- Hash is deterministic: same parameters → same hash
- No function name embedded
Centralized Verification
Core.sol verifies the signature using validateAndConsumeNonce():
// Called by services (internal to Core.sol payment functions)
Core(coreAddress).validateAndConsumeNonce(
user, // Signer's address
hashPayload, // From CoreHashUtils
executor, // Who can execute
nonce, // User's nonce
isAsyncExec, // Execution mode
signature // EIP-191 signature
);
What validateAndConsumeNonce() Does:
- Constructs full signature message with all 6 components
- Applies EIP-191 wrapping and hashing
- Recovers signer from signature using ecrecover
- Validates signer matches
userparameter - Checks nonce status (must be available for sync, reserved for async)
- Validates executor authorization (if not
0x0...0) - Marks nonce as consumed
- Optionally delegates to UserValidator contract
Message Construction
The signature message is constructed internally by Core.sol:
// Internal Core.sol message construction (simplified)
string memory message = string.concat(
AdvancedStrings.uintToString(evvmId), // "1"
",",
AdvancedStrings.addressToString(address(this)), // Core.sol address
",",
AdvancedStrings.bytes32ToString(hashPayload), // Hash from CoreHashUtils
",",
AdvancedStrings.addressToString(executor), // Authorized executor
",",
AdvancedStrings.uintToString(nonce), // User's nonce
",",
isAsyncExec ? "true" : "false" // Execution mode
);
EIP-191 Message Hashing
The message is then hashed according to EIP-191:
bytes32 messageHash = keccak256(
abi.encodePacked(
"\x19Ethereum Signed Message:\n",
AdvancedStrings.uintToString(bytes(message).length),
message
)
);
This creates the final hash that the user signs with their private key.
Complete Example: Send 0.05 ETH
Scenario: User wants to send 0.05 ETH to 0x742d...82d8c with synchronous execution
Step 1: Generate Hash Payload
import {CoreHashUtils} from "@evvm/testnet-contracts/library/signature/CoreHashUtils.sol";
address receiver = 0x742d7b6b472c8f4bd58e6f9f6c82e8e6e7c82d8c;
address token = address(0); // ETH
uint256 amount = 50000000000000000; // 0.05 ETH
uint256 priorityFee = 1000000000000000; // 0.001 ETH
// Convert address to bytes32 (left-padded)
bytes32 receiverBytes = bytes32(uint256(uint160(receiver)));
bytes32 hashPayload = CoreHashUtils.hashDataForPay(
receiverBytes,
token,
amount,
priorityFee
);
// Result: 0xa7f3c2d8e9b4f1a6c5d8e7f9b2a3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1
Step 2: Construct Signature Message
Parameters:
evvmId:1serviceAddress:0xCoreContractAddress(deployed Core.sol)hashPayload:0xa7f3c2d8e9b4f1a6c5d8e7f9b2a3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1executor:0x0000000000000000000000000000000000000000(unrestricted)nonce:42isAsyncExec:false
Final Message:
1,0xCoreContractAddress,0xa7f3c2d8e9b4f1a6c5d8e7f9b2a3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1,0x0000000000000000000000000000000000000000,42,false
Step 3: EIP-191 Formatted Hash
keccak256(abi.encodePacked(
"\x19Ethereum Signed Message:\n138",
"1,0xCoreContractAddress,0xa7f3c2d8e9b4f1a6c5d8e7f9b2a3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1,0x0000000000000000000000000000000000000000,42,false"
))
Step 4: User Signs Message
The user signs the message using MetaMask or another EIP-191 compatible wallet:
// Frontend example (ethers.js)
const message = "1,0xCoreAddress,0xa7f3...a0b1,0x0000...0000,42,false";
const signature = await signer.signMessage(message);
Step 5: Submit Transaction
// Call Core.sol payment function
Core(coreAddress).pay(
receiver,
receiverIdentity, // Empty bytes32 when using address
token,
amount,
priorityFee,
executor,
nonce,
isAsyncExec,
signature
);
Core.sol internally calls validateAndConsumeNonce() to verify the signature before processing payment.
Example with Username
Scenario: Send 0.05 ETH to username alice with async execution
Step 1: Convert Username to Bytes32
bytes32 usernameHash = keccak256(abi.encodePacked("alice"));
// Result: 0x2b3e82d9a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1
Step 2: Generate Hash Payload
bytes32 hashPayload = CoreHashUtils.hashDataForPay(
usernameHash, // Username as bytes32
address(0), // ETH
50000000000000000, // 0.05 ETH
2000000000000000 // 0.002 ETH fee
);
Step 3: Construct Signature Message
Parameters:
evvmId:1serviceAddress:0xCoreContractAddresshashPayload:0xb4c2d8e9f1a6c5d8...(from Step 2)executor:0x0000000000000000000000000000000000000000nonce:15isAsyncExec:true(async)
Final Message:
1,0xCoreContractAddress,0xb4c2d8e9f1a6c5d8e7f9b2a3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3,0x0000000000000000000000000000000000000000,15,true
Step 4: Sign and Submit
Core(coreAddress).pay(
address(0), // No direct address
usernameHash, // Username identifier
address(0), // ETH
50000000000000000,
2000000000000000,
address(0), // Unrestricted
15,
true, // Async
signature
);
Parameter Formatting
Use AdvancedStrings library for proper conversions:
import {AdvancedStrings} from "@evvm/testnet-contracts/library/utils/AdvancedStrings.sol";
// Convert types to strings
AdvancedStrings.uintToString(42); // "42"
AdvancedStrings.addressToString(0x742d...); // "0x742d7b6b..."
AdvancedStrings.bytes32ToString(0xa7f3...); // "0xa7f3c2d8..."
Signature Requirements
- Length: Exactly 65 bytes
- Structure:
[r (32 bytes)][s (32 bytes)][v (1 byte)] - V Value: 27 or 28 (automatically adjusted by wallets)
- Standard: EIP-191 compliant
Best Practices
Security
- Never reuse nonces: Each signature must have a unique nonce
- Validate parameters: Check receiver, amount, token before signing
- Use async cautiously: Async execution requires nonce reservation
Development
- Use CoreHashUtils: Don't manually construct
hashPayload - Test signature generation: Verify message format matches expected structure
- Track nonces: Query Core.sol for next available nonce
- Handle executor properly: Use
0x0...0for public, specific address for restricted
Gas Optimization
- Prefer sync execution: Async costs more gas due to nonce reservation
- Batch payments: Use
batchPay()for multiple recipients - Cache hash payload: Reuse for multiple signatures with same parameters
Related Operations
- Batch Payment Signatures - Multiple recipients
- Withdrawal Signatures - Withdraw from Core balance
- Core.sol Payment Functions - Function reference
Signatures use hash-based payload encoding and centralized verification in Core.sol for improved security and gas efficiency.