filecoin-project / ref-fvm

Reference implementation of the Filecoin Virtual Machine
https://fvm.filecoin.io/
Other
374 stars 130 forks source link

[Idea] Extensible account model #721

Open raulk opened 2 years ago

raulk commented 2 years ago

(This is a very early idea sketch; I just wanted to get it out there after brainstorming with @karim-agha around this. It's still missing a proper reflection on pros, cons, risks, security properties, and other opportunities this scheme enables.)

Context

Filecoin needs the ability to intake signed Ethereum transactions, often issued from wallets like MetaMask. This is an integral part of the Ethereum compatibility story.

Forwarders/bridge services and Ethereum proxy actors may be a solution to frame and sponsor/chaperone Filecoin messages out of Ethereum transactions. However, they feels clunky, lazy and non-native. They also summon complexity and challenges:

Proposal (conceptual design)

Key elements

The first element of the proposal is to introduce a mechanism to enable actors to promote themselves as message senders. By offering a new Wasm entrypoint authenticate, actor code can advise the FVM that it supplies custom logic to authenticate a message payload and, optionally, hoist itself as the sender.

The second element is to omitting Filecoin signatures in messages where the authentication will be performed by the sender. This requires introducing a new signature type "delegated", signifying that the authentication is delegated to the sender actor via authenticate.

The third element is to add an sself::change_code_cid privileged syscall that enables an actor to change its CodeCID. For M2, it would only be allowable from the account actor. The v9? account actor would offer a new method EnableEthTx to enable Ethereum compatibility. It's only invokable once, and on f1 addresses only (secp pubkeys). It changes its CodeCid to the one for the eaccount builtin actor (Ethereum-compatible acount) —whose bytecode offers the authenticate entrypoint— and adds the corresponding address mapping to the EVM address registry.

Note that calling the EnableEthTx method is necessary because we need to ecrecover the pubkey to generate the Ethereum address for the EVM address registry. We could do away with this step if we drop some requirements though.

Processing EVM transactions

Wallets like MetaMask submit signed Ethereum transactions through the eth_sendSignedTransaction JSON-RPC operation, offered by Filecoin clients. This method would:

  1. Validate the sig and other things (nonce, etc.)
  2. Wrap the transaction in a Filecoin message with delegated signature type
    • from is computed by performing ecrecover on the Ethereum signature and computing the f1 address for the pubkey.
    • everything else (to, nonce, value, gas params, etc.) is copied from the EVM tx.
    • the params are the full signed EVM tx.
  3. Submit it to the mempool

The FVM is now responsible for realising that the tx type is "delegated", loading the bytecode of from and invoking its authenticate entrypoint. Details on params, return value and what syscalls are allowed TBD.

eaccount's authenticate validates the signature on the inner EVM message as well as the consistency with the outer message. If successful, it returns without aborting, thus instructing the FVM to adopt the eaccount actor as the sender, using the nonce, value, etc. from the outer Filecoin message.

The FVM will now invoke the receiver (an Ethereum contract that will understand the EVM tx) using the context of the authenticated sender.

We have now managed to intake an Ethereum transaction without infecting the core protocol and without requiring clunky app-level bridges or extra actors (e.g. indirection).

Supporting other chains

This mechanism allows us to support native transactions with their relevant signature types (e.g. ecdsa with curve25519) from other chains by authenticating them in user/semi-privileged land, without leaking transaction and signature scheme details of every chain to the core protocol.

Open questions

Stebalien commented 2 years ago

TL;DR:


The third element is to add an sself::change_code_cid privileged syscall that enables an actor to change its CodeCID. For M2, it would only be allowable from the account actor.

(note: I'd like this to not be privileged to avoid users using the proxy actor upgrade pattern)

The v9? account actor would offer a new method EnableEthTx to enable Ethereum compatibility. It's only invokable once, and on f1 addresses only (secp pubkeys). It changes its CodeCid to the one for the eaccount builtin actor (Ethereum-compatible acount) —whose bytecode offers the authenticate entrypoint— and adds the corresponding address mapping to the EVM address registry.

I like the ability to use f1 addresses here, but I'm not a fan of adding an Eth specific endpoint to accounts.

But I think we can generalize this, and make it really useful elsewhere by adding a general-purpose "upgrade" method to account actors that let the owner replace the account actor's code with some other type of actor. In this case, you'd replace the actor with an EVM proxy, eaccount, etc.

This would also allow one to start out with an account actor, then upgrade to a multisig. I can see this being used all the time in practice where a service starts out by generating an HD Wallet that may never be used, then transitions it into a multisig when/if it appears on-chain. It's similar to the CREATE2 counterfactual, but it works with wallets and keys.

It would also allow users to "upgrade" accounts later if they find that they need additional security, even if that account has already been hard-coded into other actors.

Overhead of loading Wasm bytecode to validate transactions in the mpool. An attacker could send millions of invalid "delegated" transactions to force Wasm code to run for free.

We can't run arbitrary wasm bytecode when validating messages. That's not a mild inconvenience, it's a trival DoS vector.

But that's not a show-stopper:

  1. We need to charge gas for authenticate and include it in the message's gas limit.
  2. Miners can have a set of "known" and/or trusted "generalized" wallet types (wallets that implement authenticate). That way we don't execute arbitrary wasm in the message pool validator.
  3. Clients can just execute the arbitrary authenticate function:
    1. If the authenticate function succeeds, the actor is charged for gas as usual.
    2. If it fails, the miner who included the message is charged the gas (probably with some penalty multiplier).

The show-stopper is tipsets because a message can go from authenticating to not authenticating. Examples:

  1. A message with the same nonce was executed in a different block in the same tipset.
  2. Some state change in a previous block in the same tipset causes authenticate to behave differently.

If this happens, we need to charge someone for the cost of trying to validate the message and failing (otherwise this is an attack vector). But we:

Ethereum doesn't have this issue because they don't have tipsets. If an Ethereum miner includes a message in a block, they can know the exact result of executing that message before actually executing it. Unfortunately, that's not the case in Filecoin (at the moment).


Proposal:

  1. Do solve the "duplicate identity" by providing a way to upgrade a normal account into some form of EVM proxy account.
  2. Use an external forwarding service until we get rid of tipsets.
anorth commented 2 years ago

There is a lot of overlap here with an account abstraction proposal that I am soon to make – especially the delegated signature validation. Account abstraction is a very general capability that would be powerful for lots of things. I had not yet realised that it might be part of a solution to smooth hosted VM workflows. I'll write more about that shortly, and then we can look for the common ground.

Edit: and contract upgradeability is another general capability we want to add (but I don't have a proposal for).

Stebalien commented 2 years ago

Note: @anorth pointed out (on slack) that we already do charge the miner in some cases like this. Specifically, we charge the miner if:

  1. A message is included.
  2. When we go to execute that message, the sending account doesn't have the funds to cover the gas.

This can happen in one of two cases:

  1. A miner includes a message that statically should not have been included.
  2. A message in a different block in the tipset spent the funds.

We need someone to cover the gas, so we charge the miner.

We could take the same attitude here and say that a miner is effectively taking responsibility for the message validation.

The main difference is...

  1. At the moment, the miner would pay the message inclusion cost as the penalty.
  2. With this method, they'd have to pay the message inclusion cost plus the cost of the failed authenticate call, which could be substantial?

But I guess that's up to the miner.

karim-agha commented 2 years ago

The second element is to omitting Filecoin signatures in messages where the authentication will be performed by the sender. This requires introducing a new signature type "delegated", signifying that the authentication is delegated to the sender actor via authenticate.

I'm wondering if this opens an attack vector where a malicious actor would upload an actor with an authenticate implementation that authenticates everything successfully and that would let them impersonate any account on filecoin.

All known blockchain use ed25519 signatures for single singer transactions (Solana, Eth, Cosmos, etc.), and in all those instances we essentially have a transaction, a public key and a signature. We can have a generic mechanism that allows authenticating any arbitrary account by validating a signature over an arbitrary byte string:

let (account_pubkey, valid) = validate(pubkey, message_bytes, signature);

Without being specific to a particular chain, then when an account is impersonated using this method, we can "translate" a message with delegated signature type to a native FileCoin message by unwrapping the inner message.

This, however, opens a very trivial and dangerous attack vector: an attacker can grab an arbitrary old transaction belonging to an account on another chain, and use its bytestring and signature to impersonate that transaction's equivalent on FileCoin.

This can be mitigated to an extend in Ethereum transactions by having a unique ChainID for FileCoin, that would reject transactions with invalid ids.

On Solana this can be mitigated to a certain extend by rejecting transactions that have invalid recent_blockhash.

So this leads me to the conclusion that we could have a two step process:

  1. let (account_pubkey, valid) = validate(pubkey, message_bytes, signature); for initial impersonation authentication.
  2. second verification step that is runtime specific that validates chain-specific things, like chainid, recent blockhashes, etc.
Stebalien commented 2 years ago

I'm wondering if this opens an attack vector where a malicious actor would upload an actor with an authenticate implementation that authenticates everything successfully and that would let them impersonate any account on filecoin.

To switch to that actor, you'd need to first have the account's key.

All known blockchain use ed25519 signatures for single singer transactions (Solana, Eth, Cosmos, etc.), and in all those instances we essentially have a transaction, a public key and a signature. We can have a generic mechanism that allows authenticating any arbitrary account by validating a signature over an arbitrary byte string:

The "translation" component would have the power to do whatever it wants.

raulk commented 2 years ago
  • Let's generalize this. If we're willing to add EVM specific functionality, there are likely simpler solutions (adding a new signature/message type is annoying, but not that hard).

In our previous convos, we expressly decided to avoid cross-chain coupling at the protocol layer. The rationale is that we don't want to couple/marry our layer 1 with other layer 1 as that will create many problems down the road. Multiply this for every blockchain that we eventually want to support, and it quickly becomes unwieldly. There are already 3 Ethereum transaction types, and they are likely to add more going forward. That's why we settled on supporting foreign transactions at the actor level.

Stebalien commented 2 years ago

That's why we settled on supporting foreign transactions at the actor level

There are three levels:

With the current proposal, we'd still have to have chain level logic to verify the type of the sending account. In that case, we have two choices:

  1. Verify that it's an eaccount. That would be EVM specific logic at the chain layer.
  2. Verify that it has an authenticate endpoint. If we're going that way, I'd rather generalize the entire thing and allow other account types (rather than hard-code eaccounts into our account model).

I guess the core problem here is that everything in the system interacts with "accounts". Any changes to accounts will affect chain verification, so any EVM specific changes will push EVM related logic down into chain verification.

raulk commented 2 years ago

Verify that it has an authenticate endpoint. If we're going that way, I'd rather generalize the entire thing and allow other account types (rather than hard-code eaccounts into our account model).

This proposal introduces this. In other words, there is no EVM-specific chain level logic required by this proposal; that would be a different proposal.

I 100% agree with the complete generalization. @anorth's proposal likely generalises even further. My goal with this sketch was to orient us towards one particular direction, and to gauge our appetite to introduce account abstraction in the protocol and abandon the bridge/proxy idea (the sooner we settle on a direction, the better).

A further generalisation here would consist of allowing accounts to always switch to any other actor code, as long as it exposes an authenticate entrypoint. (Dangerous, that's why I opened with allowing account actors to transition only to eaccount, as a form of a poor man allowlist, and a stepping stone until we generalise further).

raulk commented 2 years ago

Tipsets really do make any solution without a separate proxy service impossible.

In this proposal, messages with delegated signature type still need to include the nonce. Wouldn't that solve your concern?

raulk commented 2 years ago

@anorth's account abstraction proposal is being incubated here (working doc): https://pl-strflt.notion.site/Account-abstraction-for-FVM-ebd15c2331d44c9aae8838bddac9e8c8

Stebalien commented 2 years ago

In this proposal, messages with delegated signature type still need to include the nonce. Wouldn't that solve your concern?

The opposite.

If a message is included but can't be executed for some reason, we charge the miner the "inclusion" cost of the message. This cost covers both the signature verification, and the data-storage cost of the message. At the moment, there are two cases where that can happen (assuming the miner didn't misbehave):

  1. The account doesn't have funds to cover the gas (e.g., a prior message in the tipset spent the funds).
  2. The account's nonce doesn't match the message nonce (e.g., a message in a prior block in the tipset incremented the nonce).

With this proposal:

  1. The "inclusion" cost will also have to cover the cost of executing the authenticate method. This could make these cases more expensive.
  2. In addition to the two cases above, authenticate could cause messages to fail for arbitrary reasons.

However, as long as:

  1. Miners can choose which types of messages to accept (which ones are "known" to have "well-behaved" authenticate methods).
  2. The cost of calling a well-behaved authenticate method is relatively cheap.

We should be fine.


This proposal introduces this. In other words, there is no EVM-specific chain level logic required by this proposal; that would be a different proposal.

Then I'm totally on-board. My concern was with the fact that we were baking EVM-specific logic into account actors.

Stebalien commented 2 years ago

@anorth's account abstraction proposal is being incubated here (working doc): https://pl-strflt.notion.site/Account-abstraction-for-FVM-ebd15c2331d44c9aae8838bddac9e8c8

This addresses most of my concerns by limiting state changes. With that version, miners don't even need to whitelist specific validation methods.

anorth commented 2 years ago

I've posted the basic account abstraction proposal for FIP discussion now in https://github.com/filecoin-project/FIPs/discussions/388.

I didn't include details of how we might use it for extensible addressing, I think we still have some bits to figure out there.