Skip to content

Commit

Permalink
feat: add L1->L2 hashes
Browse files Browse the repository at this point in the history
  • Loading branch information
PhilippeR26 committed Jul 25, 2024
1 parent 7f5e42d commit f8c1dac
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 28 deletions.
59 changes: 59 additions & 0 deletions __tests__/utils/hash.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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');
});
});
19 changes: 5 additions & 14 deletions src/provider/rpc.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -160,7 +157,7 @@ export class RpcProvider implements ProviderInterface {
.then(this.responseParser.parseL1GasPriceResponse);
}

public async getL1MessageHash(l2TxHash: BigNumberish) {
public async getL1MessageHash(l2TxHash: BigNumberish): Promise<string> {
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 } =
Expand All @@ -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) {
Expand Down
89 changes: 76 additions & 13 deletions src/utils/hash/selector.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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,
]);
}
1 change: 1 addition & 0 deletions src/utils/hash/transactionHash/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
calculateInvokeTransactionHash as v3calculateInvokeTransactionHash,
} from './v3';

export { calculateL2MessageTxHash } from './v2';
/*
* INVOKE TX HASH
*/
Expand Down
48 changes: 48 additions & 0 deletions src/utils/hash/transactionHash/v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
);
}
60 changes: 59 additions & 1 deletion www/docs/guides/L1message.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/).

Expand Down Expand Up @@ -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).
Expand All @@ -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

0 comments on commit f8c1dac

Please sign in to comment.