Cryptography

Lamina's crosschain architecture facilitates operations on various blockchains' virtual machines. Each virtual machine has tradeoffs, inter alia each have their own best-suited signature algorithms. As such, forcing all chains to share the same signature algorithm is reductive for both a gas overhead costs and wallet exclusion. Instead we make use of the best security practices on all chains, this means we can accept any signature type/size, and thus accept any wallet.

Signature Types

Type-1 Signatures – ECDSA

ECDSA, or elliptic curve digital signature algorithm, is the standard public-private key signature architecture used by Ethereum/Bitcoin wallets. Takes 32-byte hash as uint256 hash; 65-byte signature as uint8 v and uint256 r, s. Returns 0 on failure, public key and -1 on success. 65-byte public key is returned as uint8 h, uint256 x1, x2.

Type-2 Signature – SECP256K1

SECP256K1, or Standards for Efficient Cryptography Prime-256 K1, is one key signature architecture used by TON. This signature curve algorithm allows for ECDSA compatibility by allowing users to use the same 256 bit private key.

Type-3 Signature – SECP256R1

SECP256K1, or Standards for Efficient Cryptography Prime-256 R1, is another key signature architecture used by TON. This signature type enables Apple OpenSSL signing which helps abstract away superfluous action away from the user blockchain experiences. This key is normally 33 bytes but for cases like TON there are comparability assembler commands to sign under a 32 bytes scheme (P256_CHKSIGNU instead of P256_CHKSIGNS).

Type-4 Signature – EDDSA

EDDSA, or Edwards-curve Digital Signature algorithm, is a signature proof borrowing elements from ED25519 and ECDSA and is the key signature architecture used by Solana.


The signature type will be appended to our domain header along with info on blockchain chain ID. Our API expects the structure will be of the format rlp[chainId, opId, sigType, sigHash, packedUserOp].

ERC-4337 (AA) Hashing and Signatures

In all signature methods for packedUserOp, the hashed data is signed by the signer private key, yielding a hash and signature pair.

struct PackedUserOperation {
    address sender;
    uint256 nonce;
    bytes initCode;
    bytes callData;
    bytes32 accountGasLimits;
    uint256 preVerificationGas;
    bytes32 gasFees;
    bytes paymasterAndData;
    bytes signature;
}

ERC-4337 generates a domain specific hash utilizing the data on the EntryPoint contract.

function getUserOpHash(
    PackedUserOperation calldata userOp
) public view returns (bytes32) {
    return
        keccak256(abi.encode(userOp.hash(), address(this), block.chainid));
}

function hash(
    PackedUserOperation calldata userOp
) internal pure returns (bytes32) {
    return keccak256(encode(userOp));
}

function encode(
    PackedUserOperation calldata userOp
) internal pure returns (bytes memory ret) {
    address sender = getSender(userOp);
    uint256 nonce = userOp.nonce;
    bytes32 hashInitCode = calldataKeccak(userOp.initCode);
    bytes32 hashCallData = calldataKeccak(userOp.callData);
    bytes32 accountGasLimits = userOp.accountGasLimits;
    uint256 preVerificationGas = userOp.preVerificationGas;
    bytes32 gasFees = userOp.gasFees;
    bytes32 hashPaymasterAndData = calldataKeccak(userOp.paymasterAndData);

    return abi.encode(
        sender, nonce,
        hashInitCode, hashCallData,
        accountGasLimits, preVerificationGas, gasFees,
        hashPaymasterAndData
    );
}

In the above hashing method we are able to calculate on chain from the userOp:

sha3(
    rlp[
        sha3(
            rlp[
                sender.nonce
                .sha3(initCode)
                .sha3(callData)
                .accountGasLimit
                .preVerificationGas
                .gasFees
                .sha3(paymasterAndData)
            ]
        )
        .EntryPoint
        .chainId
    ]
)

Naturally the signature is not hashed in the packedUserOp, so how can we pass the data? We are able to store additional information in any byte field outside the range of the specified byte length value.

struct PackedPaymasterAndData {
  address paymaster;
  uint256 packedGas;
  address owner;
  uint256 destinationId;
  address asset;
  uint256 amount;
  uint8 sigType;
}

struct PaymasterAndData2 {
  address paymaster;
  uint128 paymasterVerificationGasLimit;
  uint128 paymasterPostOpGasLimit;
  address owner;
  uint256 destinationId;
  address paymentAsset;
  uint256 paymentAmount;
  address transferAsset;
  uint256 transferAmount;
  uint8 sigType;
}

In both our type-1 and type-2 paymasterAndData objects it can be seen we have a uint8 value sigType, or signature type. The signature type, like the packedUserOp signature field, is not used for the calculated userOp hash or paymasterAndData signature. Instead, we make use of extra bytes. The sigType specifies the signature data length.

The paymasterAndData hash is:

sha3(
    rlp[
        paymaster
        .owner
        .destinationId
        .paymentAsset
        .paymentAmount
        .transferAsset
        .transferAmount
        .sigType
    ]
)

Since the signature object of the packedUserOp is the last dynamic memory object of the userOp object, any data after the length of the signature field will not be read into an object. But to our benefit this data is still accessible. This allows us to limit the required validation data crosschain, since we do not need the full userOp validation on the target chain but require the payment to validated. This remains deterministic still since the called Escrow must also validate the crosschain sender origin, chainId, and crosschainHandler.

Last updated

© 2024 Lamina Labs. All Rights Reserved