Currently validator keys are ed25519 keys and it is very expensive to verify ed25519 signatures on Ethereum. As a result, our bridge has to be optimistic on NEAR → Eth side and rely on challenges to make sure that the block headers submitted are valid. This means that it suffers from the problems with optimistic approaches – the challenge period has to be sufficiently long to have practical security, which hinders usability significantly.
To address this, @alexatnear comes up with the following idea: we add an extra secp validator key to sign messages for the bridge. More specifically, block producers, when producing approvals, also sign the following message with their secp key:
(last_final_block_height, last_final_blocks_merkle_root, next_secp_merkle_root, next_total_stake)
and such signatures are added to the body of blocks instead of block headers.
Changes to the Bridge
Now say the block that is signed is B
, and the last final block in its ancestry is F
. last_final_block_height
is the height of F
, last_final_blocks_merkle_root
is block_merkle_root
of F
, and next_secp_merkle_root
is the merkle root of the SECP public keys and corresponding stakes of the validators in the next epoch.
Any final block can be relayed. The logic of the block relaying on ETH side is then (in pseudocode):
last_known_height
last_known_secp_merkle_root
last_known_total_stake
next_known_secp_merkle_root
next_known_total_stake
last_known_blocks_merkle_root
fn relay_block(block_height, blocks_merkle_root, cur_secp_merkle_root, cur_total_stake, next_secp_merkle_root, next_total_stake, signatures: Vec<public key, stake, signature, merkle proof>) {
assert ((cur_secp_merkle_root == last_known_secp_merkle_root && cur_total_stake == last_known_total_stake) || (cur_secp_merkle_root == next_known_secp_merkle_root && cur_total_stake == next_known_total_stake));
assert (last_known_height < block_height)
signed_stake = 0
for public_key, stake, signature, merkle_proof in signatures {
assert merkle_root_matches((public_key, stake), merkle_path, cur_secp_merkle_root)
assert signature validates(public_key, signature, (block_height, blocks_merkle_root, next_secp_merkle_root, next_total_stake))
signed_stake += stake
}
assert signed_stake > total_stake * 2 / 3
last_known_height = block_height
last_known_secp_merkle_root = cur_secp_merkle_root
next_known_secp_merkle_root = next_secp_merkle_root
// same for stakes
last_known_blocks_merkle_root = blocks_merkle_root
}
With this approach, we can calculate the new gas consumption on eth side assuming that we have 100 validators on NEAR and therefore merkle proof of size 7 * 32 bytes.
For each signature:
Call data of size (64 + 7 * 32 + 32 + 16) * 16 GAS = 5376
Signature verification = 3700
Merkle verification = 772 * 7 = 5404
Total: 14480
Some possible savings here: we don’t need to send the public key along, because ecrecover can recover it, it will save 32*16=512 gas per signature.
For 34 signatures: 492320 gas (474912 if we save those 512 gas / signature)
Cost of storage:
We overwrite 6 words: 6 * 5000 = 30000
Base call: 21000
Total: 543320 gas + some overhead for loops and arithmetic.
We can also save on not storing total stake, and instead committing to it as part of the secp_merkle_root
, it will save another 10K gas, but will add a SHA256 and a bit more overhead.
So somewhere between 500K and 600K
When we switch to the new bridge design, the change will be to commit to the state merkle root, not to the blocks_merkle_root
, but other than that it’s identical.
If a block contains most of the signatures (in particular, the signatures from the top validators that together have 34% of stake), the cost will be cheaper. E.g. today 10 signatures is enough to collect 34%, and thus the cost will be
30000 + 21000 + 14480 * 10 = 195800
Changes to the protocol
As mentioned above, we will need to change approvals and blocks. However, the bigger problem is that we need to modify the staking action. Current proposal is that we introduce a new staking action
StakeAction2 { stake: Balance, public_key: ED25519PublicKey, secp_key: SECP256K1PublicKey, bls_key: BLS12381PublicKey }
that stakes with multiple keys (we add bls key here just in case. It is not going to be used any time soon).
However, the problem is that the current staking pools, which are permissionless and do not have any access keys, will not be able to operate once we the new staking action kicks in. There are two options:
-
We ask validators to migrate to new staking pools with the new staking action once it is introduced. This will create considerable amount of trouble for validators since all the delegation needs to be moved as well and given that validators most likely do not know all their delegators, this migration could be arduous.
-
We introduce a new action
SetValidatorExtraKeys { account_id: AccountId, validator_key: ED25519PublicKey, secp_key: SECP256K1PublicKey, bls_key: BLS12381PublicKey, signature: ED25519Signature }
When this action is executed, it locates an existing validators and validator proposals matching the account_id
and validator_key
, validates that the signature
is a valid signature on all the fields excluding signature concatenated using the validator public key, and stores the secp key and the bls key in the epoch manager.
If we introduce this new action, then validators do not need to migrate their staking pools. However, the downside is that we need to introduce yet another action, which complicates things even further. For example, this means that we cannot deprecate the old StakeAction
and have to deal with it even after the protocol has switched to require secp signatures.
The upgrade process
The protocol upgrade shall be done in three steps:
- We introduce the protocol change to approvals and blocks to add secp signatures. At this step we do not enforce the presence of secp signatures.
- We introduce the new staking action
StakeAction2
. This step can be combined with step 1. If we choose to go with option 2 above, we also introduceSetValidatorExtraKeys
. - After 1 and 2 are done, we wait until all existing validators have set their secp key and then we upgrade to a new protocol that now requires the presence of secp signatures.
Drawbacks
We have mentioned above that this entire change is quite invasive and affects not only the protocol itself but also various aspects of our tooling. For example, we need to support all the new actions added, and, for new validators, we also need to implement a new version of the staking pool so that they do not have to endure the cumbersomeness of having to issue another transaction to add their secp key.
In addition, as @eatmore points out, this change implies that we tie the bridge to our protocol and if we need to change the message we sign for the bridge in the future, we have to perform a protocol upgrade just for that. Such tight coupling of bridge and the protocol is not optimal from a design perspective.