Core Signature Verification Standard
Core Signature Verification Standard
Abstract
CIP-712 defines a standard for hashing and signing typed structured data in Core Blockchain applications. It adapts the data model and \x19\x01 framing of EIP-712 to Core Blockchain while using Core network identifiers, addresses, hashing, and signatures.
The standard provides domain separation between applications, contracts, and Core networks. Replay protection within a single domain is application-specific and is not provided by CIP-712 itself.
Motivation
Signing an opaque byte string does not communicate the meaning of a message to the signer. Typed structured data gives wallets and applications the information required to present the message as named fields and gives independent implementations a deterministic way to calculate the same digest.
CIP-712 follows EIP-712 where its rules are compatible with Core Blockchain. The Core-specific rules in this document take precedence. In particular, Core uses networkId, Core addresses, SHA3-256, and Core EDDSA signatures.
Specification
The key words MUST, MUST NOT, SHOULD, and MAY in this document are to be interpreted as requirements on conforming implementations.
Hash Function
In this specification, hash means the result of Ylem's keccak256 function, which uses SHA3-256 on Core Blockchain. Implementations MUST NOT substitute Ethereum's legacy Keccak-256 when calculating CIP-712 hashes.
Typed Data
Typed data consists of a primary struct type, the definitions of all referenced struct types, and the values of the primary struct's fields. Type names, field types, field names, and field order are part of the signed data.
The canonical type encoding follows EIP-712. For example:
Mail(address to,string contents)
The type hash and struct hash are calculated as follows:
typeHash = hash(encodeType(typeOf(data)))
structHash = hash(typeHash || encodeData(data))
Values MUST be encoded according to the Ylem ABI. Core address values are 22-byte addresses and MUST be encoded as Ylem address values. Dynamic bytes and string values are represented by the hash of their contents. Nested structs are represented by their struct hash, and arrays are represented by the hash of their encoded elements.
Domain Separator
CIP-712 uses the following domain type:
EIP712Domain(string name,string version,uint256 networkId,address verifyingContract)
The literal name EIP712Domain is retained for compatibility with the Core OpenZeppelin implementation and MUST be used when calculating the domain type hash. Renaming the encoded type to CIP712Domain produces a different domain separator.
The domain type hash and domain separator are calculated as follows:
domainTypeHash = hash("EIP712Domain(string name,string version,uint256 networkId,address verifyingContract)")
domainSeparator = hash(abi.encode(
domainTypeHash,
hash(bytes(name)),
hash(bytes(version)),
networkId,
verifyingContract
))
The fields have the following meanings:
name: the human-readable name of the application or protocol.version: the current major version of the signing domain.networkId: the current Core network identifier. In Ylem contracts this value is obtained fromblock.chainid.verifyingContract: the Core address of the contract that will verify the signature.
Changing any domain field invalidates signatures created for the previous domain.
Signed Digest
The digest presented to the Core signing algorithm MUST be:
digest = hash("\x19\x01" || domainSeparator || structHash)
Both domainSeparator and structHash are exactly 32 bytes. The prefix, domain separator, and struct hash are concatenated without additional ABI padding.
Signatures
The signer signs digest using the Core signing algorithm. A Core signature is a 171-byte blob consisting of a 114-byte signature and a 57-byte public key. CIP-712 signatures MUST be handled as a single bytes value and not as an Ethereum-style (v, r, s) tuple.
Verification
A verifier MUST reconstruct the domain separator and struct hash from the expected domain and message, calculate digest, recover the signer from the Core signature, and compare the recovered address with the expected signer.
In Ylem, verification can be performed with ecrecover(bytes32 digest, bytes signature) or through a wrapper such as Core OpenZeppelin's EDDSA.recover. Implementations MUST reject signatures whose length is not 171 bytes and MUST treat signature recovery failure as invalid.
Contract Implementation
The Core OpenZeppelin EIP712 contract implements the domain separator and final digest:
bytes32 private constant _TYPE_HASH =
keccak256("EIP712Domain(string name,string version,uint256 networkId,address verifyingContract)");
function _buildDomainSeparator(bytes32 hashedName, bytes32 hashedVersion)
private
view
returns (bytes32)
{
return keccak256(
abi.encode(_TYPE_HASH, hashedName, hashedVersion, block.chainid, address(this))
);
}
function _hashTypedDataV4(bytes32 structHash) internal view returns (bytes32) {
return EDDSA.toTypedDataHash(_domainSeparatorV4(), structHash);
}
The contract does not implement application-specific struct encoding. Applications MUST define their own type hashes and construct their own struct hashes according to this specification.
Implementations MAY expose their domain through eip712Domain(). The Core OpenZeppelin implementation reports name, version, networkId, and verifyingContract.
JSON-RPC
Core clients that provide typed-data signing SHOULD expose it through xcb_signTypedData_v4. Typed-data payloads MUST use networkId, not Ethereum's chainId, and all addresses MUST be valid Core addresses for the selected network.
Usage
CIP-712 can be used for permits, orders, governance votes, meta-transactions, and other actions authorized by an off-chain signature. Applications SHOULD include all values that affect the authorized action in the signed struct.
Backward Compatibility
CIP-712 retains the EIP-712 typed-data model and final \x19\x01 framing, but it is not byte-for-byte compatible with Ethereum EIP-712. Core-specific hashing, address encoding, network identifiers, and signatures produce different results.
Existing Core applications that use the domain and digest construction specified above remain compatible. Applications MUST NOT assume that an Ethereum EIP-712 signature can be verified as a CIP-712 signature.
Security Considerations
CIP-712 does not provide replay protection within one signing domain. Applications authorizing a one-time action SHOULD include and validate a nonce, deadline, consumed-message record, or another application-specific replay-prevention mechanism.
Including networkId and verifyingContract in the domain prevents a correctly constructed signature from being reused on a different Core network or by a different contract. Applications MUST verify the complete domain and MUST ensure that wallets display the same domain and message that the verifier will hash.
Implementations MUST use the exact type definitions and field order agreed upon by the signer and verifier. A changed type name, domain type name, field order, hash function, or address encoding produces a different digest.
Submission of a valid signature by an unintended third party may be possible. Applications MUST ensure that early submission either has the exact effect authorized by the signer or is otherwise safely rejected.
Conclusion
CIP-712 provides a deterministic Core-specific procedure for hashing, signing, and verifying typed structured data while retaining the established EIP-712 framing and domain-separation model.
Copyright
Copyright and related rights waived via CC0.