Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 84 additions & 32 deletions src/services/railgun/wallets/wallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,47 +428,67 @@ export const sign7702Request = async (
transactions: (TransactionStructV2 | TransactionStructV3)[],
actionData: RelayAdapt7702.ActionDataStruct,
mnemonicPassword?: string,
ephemeralIndex?: number,
): Promise<{
authorization: Authorization;
signature: string;
executionDetails: RelayAdapt7702ExecutionDetails;
}> => {
const wallet = fullWalletForID(walletID);
const provider = getFallbackProviderForNetwork(networkName);
// Single-source every ephemeral facet on the signer that will actually produce the
// authorization below. getCurrentEphemeralAddress resolves the signer's address (honoring the
// same override/provider precedence as sign7702Request), so the authorization nonce and the
// execute nonce are read from the authorization authority itself. Reading them from
// getCurrentEphemeralWallet instead desyncs when a custom signer provider is set (authority
// != nonce-source), which silently invalidates the on-chain authorization.
const ephemeralAddress = await wallet.getCurrentEphemeralAddress(
encryptionKey,
chainId,
mnemonicPassword,
);
const nonce = await provider.getTransactionCount(ephemeralAddress, 'latest');
const executionDetails = await getRelayAdapt7702ExecutionDetails(
provider,
networkName,
ephemeralAddress,
);

const { authorization, signature } = await wallet.sign7702Request(
encryptionKey,
contractAddress,
chainId,
transactions,
actionData,
nonce,
executionDetails,
mnemonicPassword,
);
// The engine reads the override ahead of both the stored index and any custom signer provider,
// so pinning it here keeps the authorization authority, the `to` address and the nonce on one
// account. It is restored below: the override is shared state on the wallet.
const previousEphemeralWalletOverride = wallet.ephemeralWalletOverride;
if (isDefined(ephemeralIndex)) {
assertCanonicalEphemeralProvider(wallet);
wallet.ephemeralWalletOverride = await wallet.getEphemeralWallet(
encryptionKey,
chainId,
ephemeralIndex,
mnemonicPassword,
);
}

return {
authorization,
signature,
executionDetails,
};
try {
// Single-source every ephemeral facet on the signer that will actually produce the
// authorization below. getCurrentEphemeralAddress resolves the signer's address (honoring the
// same override/provider precedence as sign7702Request), so the authorization nonce and the
// execute nonce are read from the authorization authority itself. Reading them from
// getCurrentEphemeralWallet instead desyncs when a custom signer provider is set (authority
// != nonce-source), which silently invalidates the on-chain authorization.
const ephemeralAddress = await wallet.getCurrentEphemeralAddress(
encryptionKey,
chainId,
mnemonicPassword,
);
const nonce = await provider.getTransactionCount(ephemeralAddress, 'latest');
const executionDetails = await getRelayAdapt7702ExecutionDetails(
provider,
networkName,
ephemeralAddress,
);

const { authorization, signature } = await wallet.sign7702Request(
encryptionKey,
contractAddress,
chainId,
transactions,
actionData,
nonce,
executionDetails,
mnemonicPassword,
);

return {
authorization,
signature,
executionDetails,
};
} finally {
wallet.ephemeralWalletOverride = previousEphemeralWalletOverride;
}
};

export const ratchetEphemeralAddress = async (
Expand All @@ -480,6 +500,38 @@ export const ratchetEphemeralAddress = async (
return wallet.ratchetEphemeralAddress(chainId);
};

// A custom ephemeral signer provider (eg. a hardware wallet) owns keys the engine cannot derive
// from an integer index, so pinning an index there would sign with an unrelated mnemonic-derived
// account. Callers that target an index must be on the canonical HD provider.
const assertCanonicalEphemeralProvider = (wallet: RailgunWallet) => {
if (!wallet.isCanonicalEphemeralProvider()) {
throw new Error(
'Cannot target an ephemeral account by index: this wallet uses a custom ephemeral signer provider.',
);
}
};

// Resolves the address of a specific ephemeral account, so funds left behind on a rotated account
// stay reachable. `getCurrentEphemeralAddress` covers the common case of the account in use now.
export const getEphemeralAddressForIndex = async (
walletID: string,
encryptionKey: string,
networkName: NetworkName,
ephemeralIndex: number,
mnemonicPassword?: string,
): Promise<string> => {
const wallet = fullWalletForID(walletID);
assertCanonicalEphemeralProvider(wallet);
const chainId = BigInt(NETWORK_CONFIG[networkName].chain.id);
const ephemeralWallet = await wallet.getEphemeralWallet(
encryptionKey,
chainId,
ephemeralIndex,
mnemonicPassword,
);
return ephemeralWallet.address;
};

export const getCurrentEphemeralAddress = async (
walletID: string,
encryptionKey: string,
Expand Down
51 changes: 38 additions & 13 deletions src/services/transactions/tx-cross-contract-calls-7702.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
EVMGasType,
TXIDVersion,
NETWORK_CONFIG,
isDefined,
} from '@railgun-community/shared-models';
import {
GenerateTransactionsProgressCallback,
Expand Down Expand Up @@ -44,7 +45,11 @@ import { ContractTransaction } from 'ethers';
import {
createRelayAdaptShieldNFTRecipients,
} from './tx-cross-contract-calls';
import { getCurrentEphemeralAddress, sign7702Request } from '../railgun/wallets/wallets';
import {
getCurrentEphemeralAddress,
getEphemeralAddressForIndex,
sign7702Request,
} from '../railgun/wallets/wallets';
import { encodeRelayAdapt7702Execute } from '../railgun/wallets/relay-adapt-7702-execution';


Expand Down Expand Up @@ -152,6 +157,7 @@ export const gasEstimateForUnprovenCrossContractCalls7702 = async (
sendWithPublicWallet: boolean,
minGasLimit: Optional<bigint>,
mnemonicPassword?: string,
ephemeralIndex?: number,
): Promise<RailgunTransactionGasEstimateResponse> => {
try {
setCachedProvedTransaction(undefined);
Expand All @@ -165,12 +171,20 @@ export const gasEstimateForUnprovenCrossContractCalls7702 = async (
const validCrossContractCalls =
createValidCrossContractCalls(crossContractCalls);

const ephemeralAddress = await getCurrentEphemeralAddress(
railgunWalletID,
encryptionKey,
networkName,
mnemonicPassword,
);
const ephemeralAddress = isDefined(ephemeralIndex)
? await getEphemeralAddressForIndex(
railgunWalletID,
encryptionKey,
networkName,
ephemeralIndex,
mnemonicPassword,
)
: await getCurrentEphemeralAddress(
railgunWalletID,
encryptionKey,
networkName,
mnemonicPassword,
);

const relayAdaptUnshieldERC20AmountRecipients =
createRelayAdapt7702UnshieldERC20AmountRecipients(
Expand Down Expand Up @@ -233,6 +247,7 @@ export const gasEstimateForUnprovenCrossContractCalls7702 = async (
transactions,
actionData,
mnemonicPassword,
ephemeralIndex,
);

const data = encodeRelayAdapt7702Execute(
Expand Down Expand Up @@ -318,19 +333,28 @@ export const generateCrossContractCallsProof7702 = async (
minGasLimit: Optional<bigint>,
progressCallback: GenerateTransactionsProgressCallback,
mnemonicPassword?: string,
ephemeralIndex?: number,
): Promise<RelayAdapt7702Request> => {
try {
setCachedProvedTransaction(undefined);

const validCrossContractCalls =
createValidCrossContractCalls(crossContractCalls);

const ephemeralAddress = await getCurrentEphemeralAddress(
railgunWalletID,
encryptionKey,
networkName,
mnemonicPassword,
);
const ephemeralAddress = isDefined(ephemeralIndex)
? await getEphemeralAddressForIndex(
railgunWalletID,
encryptionKey,
networkName,
ephemeralIndex,
mnemonicPassword,
)
: await getCurrentEphemeralAddress(
railgunWalletID,
encryptionKey,
networkName,
mnemonicPassword,
);

const relayAdaptUnshieldERC20AmountRecipients =
createRelayAdapt7702UnshieldERC20AmountRecipients(
Expand Down Expand Up @@ -426,6 +450,7 @@ export const generateCrossContractCallsProof7702 = async (
transactions,
actionData,
mnemonicPassword,
ephemeralIndex,
);

// Construct the transaction data
Expand Down