From f8c1dacc164d487eeac014f932a395ca064e9545 Mon Sep 17 00:00:00 2001 From: PhilippeR26 Date: Thu, 25 Jul 2024 19:25:52 +0200 Subject: [PATCH] feat: add L1->L2 hashes --- __tests__/utils/hash.test.ts | 59 ++++++++++++++++ src/provider/rpc.ts | 19 ++---- src/utils/hash/selector.ts | 89 +++++++++++++++++++++---- src/utils/hash/transactionHash/index.ts | 1 + src/utils/hash/transactionHash/v2.ts | 48 +++++++++++++ www/docs/guides/L1message.md | 60 ++++++++++++++++- 6 files changed, 248 insertions(+), 28 deletions(-) diff --git a/__tests__/utils/hash.test.ts b/__tests__/utils/hash.test.ts index 2d1399794..2b24789f3 100644 --- a/__tests__/utils/hash.test.ts +++ b/__tests__/utils/hash.test.ts @@ -1,4 +1,5 @@ import { keccakBn, starknetKeccak, getSelectorFromName, getSelector } from '../../src/utils/hash'; +import { constants, hash } from '../../src'; describe('keccakBn', () => { test('should properly calculate the Keccak hash', () => { @@ -38,4 +39,62 @@ describe('getSelector', () => { test('should return the proper selector when provided a decimal string', () => { expect(getSelector('123456')).toBe('0x1e240'); }); + + test('should return the proper selector when provided a number', () => { + expect(getSelector(123456)).toBe('0x1e240'); + }); + + test('should return the proper selector when provided a bigint', () => { + expect(getSelector(123456n)).toBe('0x1e240'); + }); +}); + +describe('L1->L2 messaging', () => { + // L1 tx for a message L1->L2 + // data extracted from : + // https://sepolia.etherscan.io/tx/0xd82ce7dd9f3964d89d2eb9d555e1460fb7792be274950abe578d610f95cc40f5 + // data extracted from etherscan : + const l1FromAddress = '0x0000000000000000000000008453fc6cd1bcfe8d4dfc069c400b433054d47bdc'; + const l2ToAddress = 2158142789748719025684046545159279785659305214176670733242887773692203401023n; + const l2Selector = 774397379524139446221206168840917193112228400237242521560346153613428128537n; + const payload = [ + 4543560n, + 829565602143178078434185452406102222830667255948n, + 3461886633118033953192540141609307739580461579986333346825796013261542798665n, + 9000000000000000n, + 0n, + ]; + const l1Nonce = 8288n; + + test('solidityUint256PackedKeccak256', () => { + const kec256Hash = hash.solidityUint256PackedKeccak256(['0x100', '200', 300, 400n]); + expect(kec256Hash).toBe('0xd1e6cb422b65269603c491b0c85463295edabebfb2a6844e4fdc389ff1dcdd97'); + }); + + test('getL2MessageHash', () => { + // https://sepolia.starkscan.co/message/0x2e350fa9d830482605cb68be4fdb9f0cb3e1f95a0c51623ac1a5d1bd997c2090#messagelogs + const l1ToL2MessageHash = hash.getL2MessageHash( + l1FromAddress, + l2ToAddress, + l2Selector, + payload, + l1Nonce + ); + expect(l1ToL2MessageHash).toBe( + '0x2e350fa9d830482605cb68be4fdb9f0cb3e1f95a0c51623ac1a5d1bd997c2090' + ); + }); + + test('calculateL2MessageTxHash', () => { + // https://sepolia.starkscan.co/tx/0x067d959200d65d4ad293aa4b0da21bb050a1f669bce37d215c6edbf041269c07 + const l2TxHash = hash.calculateL2MessageTxHash( + l1FromAddress, + l2ToAddress, + l2Selector, + payload, + constants.StarknetChainId.SN_SEPOLIA, + l1Nonce + ); + expect(l2TxHash).toBe('0x67d959200d65d4ad293aa4b0da21bb050a1f669bce37d215c6edbf041269c07'); + }); }); diff --git a/src/provider/rpc.ts b/src/provider/rpc.ts index 65d8c0ac4..548e1215f 100644 --- a/src/provider/rpc.ts +++ b/src/provider/rpc.ts @@ -1,7 +1,4 @@ -import { bytesToHex } from '@noble/curves/abstract/utils'; -import { keccak_256 } from '@noble/hashes/sha3'; import type { SPEC } from 'starknet-types-07'; - import { RPC06, RPC07, RpcChannel } from '../channel'; import { AccountInvocations, @@ -34,13 +31,13 @@ import type { TransactionWithHash } from '../types/provider/spec'; import assert from '../utils/assert'; import { getAbiContractVersion } from '../utils/calldata/cairo'; import { isSierra } from '../utils/contract'; -import { addHexPrefix, removeHexPrefix } from '../utils/encode'; -import { hexToBytes, toHex } from '../utils/num'; +import { toHex } from '../utils/num'; import { wait } from '../utils/provider'; import { RPCResponseParser } from '../utils/responseParser/rpc'; import { GetTransactionReceiptResponse, ReceiptTx } from '../utils/transactionReceipt'; import { LibraryError } from './errors'; import { ProviderInterface } from './interface'; +import { solidityUint256PackedKeccak256 } from '../utils/hash'; export class RpcProvider implements ProviderInterface { public responseParser: RPCResponseParser; @@ -115,7 +112,7 @@ export class RpcProvider implements ProviderInterface { /** * Pause the execution of the script until a specified block is created. - * @param {BlockIdentifier} blockIdentifier bloc number (BigNumberisk) or 'pending' or 'latest'. + * @param {BlockIdentifier} blockIdentifier bloc number (BigNumberish) or 'pending' or 'latest'. * Use of 'latest" or of a block already created will generate no pause. * @param {number} [retryInterval] number of milliseconds between 2 requests to the node * @example @@ -160,7 +157,7 @@ export class RpcProvider implements ProviderInterface { .then(this.responseParser.parseL1GasPriceResponse); } - public async getL1MessageHash(l2TxHash: BigNumberish) { + public async getL1MessageHash(l2TxHash: BigNumberish): Promise { const transaction = (await this.channel.getTransactionByHash(l2TxHash)) as TransactionWithHash; assert(transaction.type === 'L1_HANDLER', 'This L2 transaction is not a L1 message.'); const { calldata, contract_address, entry_point_selector, nonce } = @@ -173,13 +170,7 @@ export class RpcProvider implements ProviderInterface { calldata.length - 1, ...calldata.slice(1), ]; - const myEncode = addHexPrefix( - params.reduce( - (res: string, par: BigNumberish) => res + removeHexPrefix(toHex(par)).padStart(64, '0'), - '' - ) - ); - return addHexPrefix(bytesToHex(keccak_256(hexToBytes(myEncode)))); + return solidityUint256PackedKeccak256(params); } public async getBlockWithReceipts(blockIdentifier?: BlockIdentifier) { diff --git a/src/utils/hash/selector.ts b/src/utils/hash/selector.ts index 063b662a8..87d4dd918 100644 --- a/src/utils/hash/selector.ts +++ b/src/utils/hash/selector.ts @@ -1,12 +1,13 @@ import { keccak } from '@scure/starknet'; - +import { keccak_256 } from '@noble/hashes/sha3'; +import { bytesToHex } from '@noble/curves/abstract/utils'; import { MASK_250 } from '../../constants'; import { BigNumberish } from '../../types'; import { addHexPrefix, removeHexPrefix, utf8ToArray } from '../encode'; -import { hexToBytes, isHex, isStringWholeNumber, toHex, toHexString } from '../num'; +import { hexToBytes, isBigInt, isHex, isNumber, isStringWholeNumber, toHex } from '../num'; /** - * Calculate the hex-string Keccak hash for a given BigNumberish + * Calculate the hex-string Starknet Keccak hash for a given BigNumberish * * @param value value to hash * @returns hex-string Keccak hash @@ -24,7 +25,7 @@ export function keccakBn(value: BigNumberish): string { /** * [internal] - * Calculate hex-string Keccak hash for a given string + * Calculate hex-string Starknet Keccak hash for a given string * * String -> hex-string Keccak hash * @returns format: hex-string @@ -69,9 +70,9 @@ export function getSelectorFromName(funcName: string) { } /** - * Calculate the hex-string selector from a given abi function name, decimal string or hex string + * Calculate the hex-string selector from a given abi function name or of any representation of number. * - * @param value hex-string | dec-string | ascii-string + * @param value ascii-string | hex-string | dec-string | number | BigInt * @returns hex-string selector * @example * ```typescript @@ -83,14 +84,76 @@ export function getSelectorFromName(funcName: string) { * * const selector3: string = getSelector("123456"); * // selector3 = "0x1e240" + * + * const selector4: string = getSelector(123456n); + * // selector4 = "0x1e240" * ``` */ -export function getSelector(value: string) { - if (isHex(value)) { - return value; - } - if (isStringWholeNumber(value)) { - return toHexString(value); - } +export function getSelector(value: string | BigNumberish) { + if (isNumber(value) || isBigInt(value)) return toHex(value); + if (isHex(value)) return value; + if (isStringWholeNumber(value)) return toHex(value); return getSelectorFromName(value); } + +/** + * Solidity hash of an array of uint256 + * @param {BigNumberish[]} params an array of uint256 numbers + * @returns the hash of the array of Solidity uint256 + * @example + * ```typescript + * const result = hash.solidityUint256PackedKeccak256(['0x100', '200', 300, 400n]); + * // result = '0xd1e6cb422b65269603c491b0c85463295edabebfb2a6844e4fdc389ff1dcdd97' + * ``` + */ +export function solidityUint256PackedKeccak256(params: BigNumberish[]): string { + const myEncode = addHexPrefix( + params.reduce( + (res: string, par: BigNumberish) => res + removeHexPrefix(toHex(par)).padStart(64, '0'), + '' + ) + ); + return addHexPrefix(bytesToHex(keccak_256(hexToBytes(myEncode)))); +} + +/** + * Calculate the L2 message hash related by a message L1->L2 + * @param {BigNumberish} l1FromAddress L1 account address that paid the message. + * @param {BigNumberish} l2ToAddress L2 contract address to execute. + * @param {string | BigNumberish} l2Selector can be a function name ("bridge_withdraw") or a number (BigNumberish). + * @param {RawCalldata} l2Calldata an array of BigNumberish of the raw parameters passed to the above function. + * @param {BigNumberish} l1Nonce The nonce of the L1 account. + * @returns {string} hex-string of the L2 transaction hash + * @example + * ```typescript + * const l1FromAddress = "0x0000000000000000000000008453fc6cd1bcfe8d4dfc069c400b433054d47bdc"; + * const l2ToAddress = 2158142789748719025684046545159279785659305214176670733242887773692203401023n; + * const l2Selector = 774397379524139446221206168840917193112228400237242521560346153613428128537n; + * const payload = [ + * 4543560n, + * 829565602143178078434185452406102222830667255948n, + * 3461886633118033953192540141609307739580461579986333346825796013261542798665n, + * 9000000000000000n, + * 0n, + * ]; + * const l1Nonce = 8288n; + * const result = hash.getL2MessageHash(l1FromAddress, l2ToAddress, l2Selector, payload, l1Nonce); + * // result = "0x2e350fa9d830482605cb68be4fdb9f0cb3e1f95a0c51623ac1a5d1bd997c2090" + * ``` + */ +export function getL2MessageHash( + l1FromAddress: BigNumberish, + l2ToAddress: BigNumberish, + l2Selector: string | BigNumberish, + l2Calldata: BigNumberish[], + l1Nonce: BigNumberish +): string { + return solidityUint256PackedKeccak256([ + l1FromAddress, + l2ToAddress, + l1Nonce, + l2Selector, + l2Calldata.length, + ...l2Calldata, + ]); +} diff --git a/src/utils/hash/transactionHash/index.ts b/src/utils/hash/transactionHash/index.ts index cc0a2e3cf..f0a28d6f4 100644 --- a/src/utils/hash/transactionHash/index.ts +++ b/src/utils/hash/transactionHash/index.ts @@ -22,6 +22,7 @@ import { calculateInvokeTransactionHash as v3calculateInvokeTransactionHash, } from './v3'; +export { calculateL2MessageTxHash } from './v2'; /* * INVOKE TX HASH */ diff --git a/src/utils/hash/transactionHash/v2.ts b/src/utils/hash/transactionHash/v2.ts index 253824a6b..1ba81088c 100644 --- a/src/utils/hash/transactionHash/v2.ts +++ b/src/utils/hash/transactionHash/v2.ts @@ -8,6 +8,7 @@ import { StarknetChainId, TransactionHashPrefix } from '../../../constants'; import { BigNumberish, RawCalldata } from '../../../types'; import { starkCurve } from '../../ec'; import { toBigInt } from '../../num'; +import { getSelector } from '../selector'; /** * Compute pedersen hash from data @@ -127,3 +128,50 @@ export function calculateTransactionHash( [nonce] ); } + +/** + * Calculate the L2 transaction hash generated by a message L1->L2 + * @param {BigNumberish} l1FromAddress L1 account address that paid the message. + * @param {BigNumberish} l2ToAddress L2 contract address to execute. + * @param {string | BigNumberish} l2Selector can be a function name ("bridge_withdraw") or a number (BigNumberish). + * @param {RawCalldata} l2Calldata an array of BigNumberish of the raw parameters passed to the above function. + * @param {BigNumberish} l2ChainId L2 chain ID : from constants.StarknetChainId.xxx + * @param {BigNumberish} l1Nonce The nonce of the L1 account. + * @returns {string} hex-string of the L2 transaction hash + * @example + * ```typescript + * const l1FromAddress = "0x0000000000000000000000008453fc6cd1bcfe8d4dfc069c400b433054d47bdc"; + * const l2ToAddress = 2158142789748719025684046545159279785659305214176670733242887773692203401023n; + * const l2Selector = 774397379524139446221206168840917193112228400237242521560346153613428128537n; + * const payload = [ + * 4543560n, + * 829565602143178078434185452406102222830667255948n, + * 3461886633118033953192540141609307739580461579986333346825796013261542798665n, + * 9000000000000000n, + * 0n, + * ]; + * const l1Nonce = 8288n; + * const result = hash.calculateL2MessageTxHash(l1FromAddress, l2ToAddress, l2Selector, payload, constants.StarknetChainId.SN_SEPOLIA, l1Nonce); + * // result = "0x67d959200d65d4ad293aa4b0da21bb050a1f669bce37d215c6edbf041269c07" + * ``` + */ +export function calculateL2MessageTxHash( + l1FromAddress: BigNumberish, + l2ToAddress: BigNumberish, + l2Selector: string | BigNumberish, + l2Calldata: RawCalldata, + l2ChainId: StarknetChainId, + l1Nonce: BigNumberish +): string { + const payload = [l1FromAddress, ...l2Calldata]; + return calculateTransactionHashCommon( + TransactionHashPrefix.L1_HANDLER, + 0, + l2ToAddress, + getSelector(l2Selector), + payload, + 0, + l2ChainId, + [l1Nonce] + ); +} diff --git a/www/docs/guides/L1message.md b/www/docs/guides/L1message.md index d4f8ecc30..22708d55b 100644 --- a/www/docs/guides/L1message.md +++ b/www/docs/guides/L1message.md @@ -8,7 +8,7 @@ You can exchange messages between L1 & L2 networks: - L2 Starknet mainnet ↔️ L1 Ethereum. - L2 Starknet testnet ↔️ L1 Sepolia ETH testnet. -- L2 local Starknet devnet ↔️ L1 local ETH testnet (Ganache, ...). +- L2 local Starknet devnet ↔️ L1 local ETH testnet (anvil, ...). You can find an explanation of the global mechanism [here](https://docs.starknet.io/documentation/architecture_and_concepts/L1-L2_Communication/messaging-mechanism/). @@ -48,6 +48,52 @@ const responseEstimateMessageFee = await provider.estimateMessageFee({ If the fee is paid in L1, the Cairo contract at `to_Address` is automatically executed, function `entry_point_selector` (the function shall have a decorator `#[l1_handler]` in the Cairo code, with a first parameter called `from_address: felt252`). The payload shall not include the `from_address` parameter. +### L1 ➡️ L2 hashes + +Starknet.js proposes 2 functions to calculate hashes related to a L1 ➡️ L2 message : + +- The L2 message hash: + For a L1 tx requesting a message L1->L2, some data extracted from etherscan : https://sepolia.etherscan.io/tx/0xd82ce7dd9f3964d89d2eb9d555e1460fb7792be274950abe578d610f95cc40f5 + + ```typescript + const l1FromAddress = '0x0000000000000000000000008453fc6cd1bcfe8d4dfc069c400b433054d47bdc'; + const l2ToAddress = 2158142789748719025684046545159279785659305214176670733242887773692203401023n; + const l2Selector = 774397379524139446221206168840917193112228400237242521560346153613428128537n; + const payload = [ + 4543560n, + 829565602143178078434185452406102222830667255948n, + 3461886633118033953192540141609307739580461579986333346825796013261542798665n, + 9000000000000000n, + 0n, + ]; + const l1Nonce = 8288n; + const l1ToL2MessageHash = hash.getL2MessageHash( + l1FromAddress, + l2ToAddress, + l2Selector, + payload, + l1Nonce + ); + // l1ToL2MessageHash = '0x2e350fa9d830482605cb68be4fdb9f0cb3e1f95a0c51623ac1a5d1bd997c2090' + ``` + + Can be verified here : https://sepolia.starkscan.co/message/0x2e350fa9d830482605cb68be4fdb9f0cb3e1f95a0c51623ac1a5d1bd997c2090#messagelogs + +- The L2 transaction hash: + For the same message: + ```typescript + const l1ToL2TransactionHash = hash.calculateL2MessageTxHash( + l1FromAddress, + l2ToAddress, + l2Selector, + payload, + constants.StarknetChainId.SN_SEPOLIA, + l1Nonce + ); + // l1ToL2TransactionHash = '0x67d959200d65d4ad293aa4b0da21bb050a1f669bce37d215c6edbf041269c07' + ``` + Can be verified here : https://sepolia.starkscan.co/tx/0x067d959200d65d4ad293aa4b0da21bb050a1f669bce37d215c6edbf041269c07 + ## L2 ➡️ L1 messages To send a message to L1, you will just invoke a Cairo contract function, paying a fee that will pay all the processes (in L1 & L2). @@ -63,3 +109,15 @@ const { suggestedMaxFee: estimatedFee1 } = await account0.estimateInvokeFee({ ``` The result is in `estimatedFee1`, of type BN. + +### L2 ➡️ L1 hash + +Starknet.js proposes a function to calculate the L1 ➡️ L2 message hash : + +```typescript +const l2TransactionHash = '0x28dfc05eb4f261b37ddad451ff22f1d08d4e3c24dc646af0ec69fa20e096819'; +const l1MessageHash = await provider.getL1MessageHash(l2TransactionHash); +// l1MessageHash = '0x55b3f8b6e607fffd9b4d843dfe8f9b5c05822cd94fcad8797deb01d77805532a' +``` + +Can be verified here : https://sepolia.voyager.online/tx/0x28dfc05eb4f261b37ddad451ff22f1d08d4e3c24dc646af0ec69fa20e096819#messages