Nick’s method — Ethereum Keyless Execution

Yamen Merhi
10 min readJan 22, 2023

This article will provide a detailed explanation of “Nick’s method”, a Keyless execution/deployment method widely referenced in various Ethereum Improvement Proposals but not often fully explained.

We’ll cover its origin, how it functions, and the benefits it offers such as reduced trust requirements and the ability for multi-chain deployment on the same address.

Table Of Content

Who is Nick ?

Nick’s Method was named after Nick Johnson, a Lead developer of ENS and an alum of Ethereum Foundation, due to his work in creating the first script for this technique.

The method was inspired by an idea proposed by Vitalik Buterin, the founder of Ethereum, that a transaction can be valid even if it is not signed by a private key.

This is what led to the use of the term “Keyless”, where no private key was associated with the transaction to be valid.

How does it work ?

Before starting with how it works, it is essential to know in a high-level way, how an Ethereum Transaction is constructed.

How a Transaction is Constructed ?

The code below shows a simple Ethereum Tx of type 0 (Legacy)

{
nonce: "0x00",
gasPrice: "0x09184e72a000",
gasLimit: "0x27100",
to: "0x0000000000000000000000000000000000000000",
value: "0x01",
data: "0x7f7465737432000000000000000000000000000000000000000000000000000000600057,
}

The JSON fields state the address being interacted with, the amount of value, the data, the gasPrice, and gasLimit.

After signing the Tx with a private key, we will have 3 more fields related to the transaction’s signature (v, r, s):

{
nonce: "0x00",
gasPrice: "0x09184e72a000",
gasLimit: "0x27100",
to: "0x0000000000000000000000000000000000000000",
value: "0x01",
data: "0x7f7465737432000000000000000000000000000000000000000000000000000000600057",
v: "0x26",
r: "0x223a7c9bcf5531c99be5ea7082183816eb20cfe0bbc322e97cc5c7f71ab8b20e",
s: "0x2aadee6b34b45bb15bc42d9c09de4a6754e7000908da72d48cc7704971491663,
}

When signing, any change in one of the main Transaction fields, such as nonce, gasPrice, gasLimit, to, value and data will result in a different signature value.

As you can see, there is no “from” field in the transaction, so how would the network know who is executing the transaction ? From whom the gasFees and value being sent will be deducted ?

The network will use the ecrecover function that takes a message and a signature as parameters and will recover the public key => address of the signer from it.

In this context, the main transaction acts as the message, with v,r, and s serving as the corresponding signature.

// Pseudo-code :)

// When Signing

const addressA = '0x067024faa81ACBF984EEA0E4E75Fcc3F44558AfD';
const addressAPrvtKey = 'b37ff986840fc14c956d74a7a3375488cce495d7fe36cfa5e8201df1f1a03aaf';

const {v,r,s} = sign(Transaction, addressAPrvtKey);

> v: 0x25
> r: 0x79855f28bdc327adbcbf85d32cb76b9aeef67dc5b9c4dafbd0a94ad3757ec501
> s: 0x3a51556f88edc2e218c5b6c540662bf289a09f16e3a0f505fcfe435dfb490a22

// When Recovering

const addressRecovered = ecrecover(Transaction, v, r, s);

> addressRecovered: 0x067024faa81ACBF984EEA0E4E75Fcc3F44558AfD

As shown in the code above, address A is signing the transaction. When the transaction is passed to the network, it will utilize the “ecrecover” function on the transaction and the associated signature parameters to identify address A as the sender of the transaction, responsible for paying the gas fees, etc.

It doesn’t matter who forward the transaction to the network because as stated before, the address executing the transaction is the one recovered from the transaction and the signature not necessarly the person forwarding (broadcasting) the transaction to the network.

Messing Up with the Transaction

Since the address originating the transaction is recovered from the Transaction itself and the signature fields, what happens if we mess up with one of the signature parameters ?

It turns out that “ecrecover” will work fine with ~most~ of the randomly generated v, r, s values, and will return a valid Ethereum address corresponding to the message and the signature.

// Pseudo-code :)

If v, r, s were changed to:

> v: 0x25
> r: 0x1212121212121212121212121212121212121212121212121212121212121212 // Human generated & Random
> s: 0x0000000000000000000000000000000001000000000000000000000000000000 // Human generated & Random

// When Recovering

const addressRecovered = ecrecover(Transaction, v, r, s);


// Return a random ethereum address

> addressRecovered: 0xe2140bdbe71cdf1d1df3a6b5d85939d1ad313722

Usually, the v, r, and s values are generated whenever an address signs a message with the private key associated.

Although the address recovered didn’t sign the Transaction himself, the Transaction (Message) with these randomly generated signature values will return its address (address recovered).

Executing from the address recovered

Even with the alteration of v, r, and s values to randomly generated values, the transaction will not be considered as “invalid”. It will still be executed successfully from the recovered address, given that the address recovered from the transaction and the signature has sufficient funds to cover the gas fees.

In this case, the solution would be to manually fund the address recovered with some ether so the gasFees can be deducted from its balance.

After funding the address recovered, and broadcasting the transaction to the network, the transaction will be successfully executed from the address recovered.

The probability of having the address recovered used by someone else (controlled by a known private key) is extremly low, which make funding it and assuming the nonce for it safe.

Summary

In summary, this method allows the execution of a transaction without being signed by a private key (Keyless), thus generating a “single-use uncontrollable address” just for the purpose of executing a transaction from it.

Real-World Usecases

At first, it’s hard to predict how this method can be used. Why would someone execute a transaction from an uncontrolled address ? Why not from his main address ?

Minimizing Trust

As described in Nick’s article, How to send Ether to 11,440 people, this method was used to send Ether to 11,440 addresses on the blockchain following the DAO Hack.

Given that the funds were controlled by a multi-sig, it was really hard and resource-consuming to require multiple signatures each time they need to send the funds from the multi-sig to an address. That could take forever, and will consume a lot of gas.

Also sending the funds to one controlled address and putting trust in this address to send the ether to all of the other 11,440 was not an option.

Are they going to spend a week painstakingly signing 11,440 individual transactions? Are they going to send the whole amount to an account controlled by a single individual so they can make the payments ?

It’s possible to make all the transfers trustlessly, in a manner that requires only a single transfer by the trustees— Nick Johnson

In short, the solution was to create a MultiSend contract that sends an array of values (ethers) to an array of recipients:

  • To reach 11,140 addresses, the total tx needed is 104 different tx.
    Generate 104 transactions that interact with MultiSend and send Ether to ~110 addresses each (because of the gas limit).
  • Mess up with the signature of the txs generated.
  • Recover with “ecrecover” 104 uncontrolled addresses.
  • Generate a tx that interacts with MultiSend again to fund ether to the 104 addresses.
  • Modify the signature then recover an uncontrolled address.
  • Send all the needed funds from the multi-sig to the last recovered uncontrolled address.
  • Broadcast the transaction that funds the 104 addresses from the last recovered uncontrolled address to the network.
  • Broadcast the transactions that fund 110 individual addresses from the 104 addresses to the network.
How to send Ether to 11,440 people — Nick Johnson

.. starting the whole process that will result in sending ether to everyone on the list — all with only a single signature required, and without needing to trust an individual with the funds.

Multi-chain Deployment on the same address

A contract address is generated based on two factors: the sender's address and his nonce.

Therefore, it is technically possible to deploy contracts at the same address by broadcasting the contract deployment transaction from the same address with the same nonce on different networks, even without using Nick’s method. However, what benefits does Nick’s method offer that this method doesn’t ?

First, the problem with this type of deployment is that:

  • It requires maintenance and manual work, so someone needs to preserve the private key that controls the address safely.
  • There is a possible probability of messing with the nonces and having a nonce higher than the nonce used to deploy and losing the possibility to deploy the contract at the same address.

Registries and factories are typically the types of contracts that need to be deployed at the same address on multiple chains. This requires preserving the private key securely to ensure that the registry or factory has the same address on different chains that may be created in the future.

With the growing popularity of smart contract accounts and wallets, it’s becoming increasingly common to see accounts and wallets deployed at the same address across multiple networks. To make this possible, a CREATE2 Factory contract needs to be deployed on different chains with the same address.

Nick’s method offers a simple solution to deploy contracts at the same address on multiple chains, by creating a transaction that is sent to the network from an uncontrolled address.

The Steps would be:

  • Generate a tx of type 0 — Legacy.
{
nonce: "0x00",
gasPrice: "0x09184e72a000",
gasLimit: "0x27100",
value: "0x00",
data: "0x<bytecode of the contract>,
}
  • Add a v field with a value of “0x1b” (27). (You’ll see why later 👀)
  • Add random values for r, s.
{
nonce: "0x00",
gasPrice: "0x09184e72a000",
gasLimit: "0x27100",
value: "0x00",
data: "0x<bytecode of the contract>",
v: "0x1b",
r: "0x0000000001000000000000000000000001000000000000000000000000100000",
s: "0x1212121212121212121212121212121212121212121212121212121212121212",

}
  • Recover the address of the deployer using “ecrecover”.
  • Serialize the transaction to generate the raw transaction ready for broadcasting on the Ethereum network.

You’ll have the rawTx, the deployer address, also you can get the address of the contract that will be created.

  • You fund the deployer address with gasPrice * gasLimit.
  • Connect to different networks and Broadcast the rawTx.
const Web3 = require("web3");
const web3 = new Web3(/** Connect to different networks*/);

const rawTx = '0xf9074b808506400000008307a1208080b906f8600560005560c0604052601460809081527f53696d706c6520436f6e7472616374204e616d6500000000000000000000000060a05260019061004190826100f3565b5034801561004e57600080fd5b506101b2565b634e487b7160e01b600052604160045260246000fd5b600181811c9082168061007e57607f821691505b60208210810361009e57634e487b7160e01b600052602260045260246000fd5b50919050565b601f8211156100ee57600081815260208120601f850160051c810160208610156100cb5750805b601f850160051c820191505b818110156100ea578281556001016100d7565b5050505b505050565b81516001600160401b0381111561010c5761010c610054565b6101208161011a845461006a565b846100a4565b602080601f831160018114610155576000841561013d5750858301515b600019600386901b1c1916600185901b1785556100ea565b600085815260208120601f198616915b8281101561018457888601518255948401946001909101908401610165565b50858210156101a25787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b610537806101c16000396000f3fe6080604052600436106100655760003560e01c8063c47f002711610043578063c47f0027146100ca578063f2c9ecd8146100ea578063f38fb65b1461010857600080fd5b806317d7de7c1461006a5780633fb5c1cb14610095578063568a1c69146100b7575b600080fd5b34801561007657600080fd5b5061007f61011d565b60405161008c9190610283565b60405180910390f35b3480156100a157600080fd5b506100b56100b03660046102d8565b600055565b005b6100b56100c5366004610307565b6101af565b3480156100d657600080fd5b506100b56100e5366004610307565b610205565b3480156100f657600080fd5b5060005460405190815260200161008c565b34801561011457600080fd5b506100b5610215565b60606001805461012c906103b8565b80601f0160208091040260200160405190810160405280929190818152602001828054610158906103b8565b80156101a55780601f1061017a576101008083540402835291602001916101a5565b820191906000526020600020905b81548152906001019060200180831161018857829003601f168201915b5050505050905090565b60323410156102055760405162461bcd60e51b815260206004820152601960248201527f4e6f7420656e6f7567682076616c75652070726f76696465640000000000000060448201526064015b60405180910390fd5b60016102118282610441565b5050565b60405162461bcd60e51b815260206004820152603660248201527f546172676574436f6e74726163743a72657665727443616c6c3a20746869732060448201527f66756e6374696f6e20686173207265766572746564210000000000000000000060648201526084016101fc565b600060208083528351808285015260005b818110156102b057858101830151858201604001528201610294565b818111156102c2576000604083870101525b50601f01601f1916929092016040019392505050565b6000602082840312156102ea57600080fd5b5035919050565b634e487b7160e01b600052604160045260246000fd5b60006020828403121561031957600080fd5b813567ffffffffffffffff8082111561033157600080fd5b818401915084601f83011261034557600080fd5b813581811115610357576103576102f1565b604051601f8201601f19908116603f0116810190838211818310171561037f5761037f6102f1565b8160405282815287602084870101111561039857600080fd5b826020860160208301376000928101602001929092525095945050505050565b600181811c908216806103cc57607f821691505b6020821081036103ec57634e487b7160e01b600052602260045260246000fd5b50919050565b601f82111561043c57600081815260208120601f850160051c810160208610156104195750805b601f850160051c820191505b8181101561043857828155600101610425565b5050505b505050565b815167ffffffffffffffff81111561045b5761045b6102f1565b61046f8161046984546103b8565b846103f2565b602080601f8311600181146104a4576000841561048c5750858301515b600019600386901b1c1916600185901b178555610438565b600085815260208120601f198616915b828110156104d3578886015182559484019460019091019084016104b4565b50858210156104f15787850151600019600388901b60f8161c191681555b5050505050600190811b0190555056fea2646970667358221220e33436a76b28a7ded5996fad916166dc8b63f86b65336f728d944e1b9252427b64736f6c634300080f00331ba0b0506c61293520454796fae142b4e173251fa5d91a7d6506cc0ca0669708065da00cd51f3367e8d81c6afa79ea89521b6b8dd777d23d26535bad37bc8d83f39c70'

async function sendRawTx() {
await web3.eth.sendSignedTransaction(rawTx)
.on('receipt', console.log);
}

sendRawTx();

So why we are getting the same address with this method ? Mainly because the address recovered is uncontrolled, meaning that the nonce is currently 0 on all chains. When this address deploys a contract, the address of the contract deployed is depending on the deployer nonce (0, same on all chains) and the address of the deployer (same on all chains). In this way, we can ensure that the contract to create will be deployed on the same address on all chains.

With this method, there is no need to safeguard a private key or need to be concerned about nonce being a specific value because it's going to be always zero for an uncontrolled address. The raw transaction can be made public and anyone who wants to deploy the contract can simply fund the address of the deployer with the required fees and broadcast the raw transaction to the network.

It is also possible to try different combinations of r and s values so the address of the contract that will be created would be a vanity address with special characters, like 0xFaC100450Af66d838250EA25a389D8Cd09062629 starting with 0xFaC10 Factory.

EIP-155 and ChainId

EIP-155 introduced the chainId as a measure to prevent transactions replay attacks by ensuring that transactions are only executed on the intended blockchain network, similar to how nonces prevent a transaction from being repeated multiple times on the same chain.

Example: Bob has 10 Ether on Ethereum and 10 LYX on LUKSO, and wants to send Alice 3 Ethers. If he constructs the transaction without replay protection (chainId), it is possible for Alice to re-broadcast the same transaction on LUKSO and receive 3 LYX from Bob’s address.

The chainId is included in the v value of the signature with the following formulas:

v = CHAIN_ID * 2 + 35 || v = CHAIN_ID * 2 + 36

This is problematic for applying specific usecases for nick’s method (Multi-chain deployment on the same address), where the transaction needs to be broadcasted on multiple chains without any modifications, including the v value. If the v value is changed, it will alter the signature and subsequently the deployer address recovered from the transaction and the signature, leading to different addresses for the contract on each chain, thereby defeating the purpose of deploying on the same address across different chains.

To resolve this problem and not having a v value related to a specific chain and to enable multi-chain transactions without the effect of the chainId, transactions can be executed without including the chainId and with a v value of 27 (0x1b in hex). This v value of 27 is the value used for transactions prior to the implementation of EIP-155.

Edge Cases

When using Nick’s method for the purpose of Multi-chain Deployment on the same address, it’s important to keep in mind that the raw transaction generated cannot be altered afterward. This is because any changes made to the transaction will affect the resultant contract and deployer address.

Therefore, it’s crucial to consider future gas prices when specifying the gasPrice value in the transaction. It’s recommended to set the gasPrice to a higher value in order to support potential increases in gas prices on other chains. So if you set the gasPrice in the transaction as 100 gwei, and in the future in chain B the gasPrice was 150, sending the rawTx generated in chain B will cause a revert.

Additionally, it is important to note that some networks nodes may block transactions that do not use the EIP155 protection, which could also be a limitation to consider. Therefore, more work need to be done on the node level to allow this kind of transactions with — rpc.allow-unprotected-txs flag.

The term unprotected transaction is used to emphasize on the point where if v didn’t include the chainId, it’s possible to replay this transaction on other network. For our usecase, it’s an intended behavior, but for other scenarios it could be harmful. Like Alice and Bob scenario above.

NPM Package

Given the limited documentation and tools available for using Nick’s method, I have created this article and an npm package that facilitates generating these kinds of transactions.

End of the article.
Connect with me on Twitter, Github, and LinkedIn.

References :

--

--

Yamen Merhi

Smart Contracts Engineer @LUKSO Twitter: 0xYamen Github: YamenMerhi