omgnetwork / immutability-eth-plugin

Other
0 stars 1 forks source link

Rollup support #37

Open InoMurko opened 3 years ago

InoMurko commented 3 years ago

API would need to support:

const nonce = await this.signer.getTransactionCount()
    const contractFunction = async (gasPrice): Promise<TransactionReceipt> => {
      this.logger.info('Submitting appendSequencerBatch transaction', {
        gasPrice,
        nonce,
        contractAddr: this.chainContract.address,
      })
      const tx = await this.chainContract.appendSequencerBatch(batchParams, {
        nonce,
        gasPrice,
      })
      this.logger.info('Submitted appendSequencerBatch transaction', {
        txHash: tx.hash,
        from: tx.from,
      })
      this.logger.debug('appendSequencerBatch transaction data', {
        data: tx.data,
      })
      return this.signer.provider.waitForTransaction(
        tx.hash,
        this.numConfirmations
      )
    }
    return this._submitAndLogTx(contractFunction, 'Submitted batch!')
InoMurko commented 3 years ago
const APPEND_SEQUENCER_BATCH_METHOD_ID = 'appendSequencerBatch()'

const appendSequencerBatch = async (
  OVM_CanonicalTransactionChain: Contract,
  batch: AppendSequencerBatchParams,
  options?: TransactionRequest
): Promise<TransactionResponse> => {
  const methodId = keccak256(
    Buffer.from(APPEND_SEQUENCER_BATCH_METHOD_ID)
  ).slice(2, 10)
  const calldata = encodeAppendSequencerBatch(batch)
  return OVM_CanonicalTransactionChain.signer.sendTransaction({
    to: OVM_CanonicalTransactionChain.address,
    data: '0x' + methodId + calldata,
    ...options,
  })
}

since the data is sent to Ethereum as calldata it is enough if the api support just that. We let the encoding happen in the Sequencer node but remove the signing. So the API would be:

InoMurko commented 3 years ago

Ah okay, now I understand, I't not only the Sequencer that needs API support for transaction batches but also the Proposer for state roots. And these two actors could be separate PKys!

InoMurko commented 3 years ago

Perhaps a better approach would be to offer signing capabilities, just like hardware wallets do: https://github.com/ethers-io/ethers.js/blob/master/packages/hardware-wallets/src.ts/ledger.ts

Also, having Ethers support would mean the integration is easier.

NOT A GOOD IDEA: because it allows the vault to sign any kind of transaction...

InoMurko commented 3 years ago

The purpose of this excersise is to pull private keys needed to interact with Rollup contracts. And to have a good private key managment, because we never see the private key, or able to retrieve one.

I've identified two PKs:

The SEQUENCER_PRIVATE_KEY, PROPOSER_PRIVATE_KEY interacts with the following contracts:

OVM_StateCommitmentChain.sol
`appendStateBatch/2`
OVM_CanonicalTransactionChain.sol
`appendSequencerBatch/1`
OVM_CanonicalTransactionChain.sol
`appendQueueBatch/1` (body commented out)

L1_WALLET_KEY interacts with the following contracts:

OVM_L1CrossDomainMessenger.sol
`relayMessage/5` 

OVM_L1CrossDomainMessengerFast.sol
`relayMessage/5`

OVM_L2CrossDomainMessenger.sol
`relayMessage/4`

OVM_L1MultiMessageRelayer.sol
`batchRelayMessages/1`

It is true that anyone can relay messages. So on first look it may seem counter-intuitive to add the relayer into Vault. But it’s more so that if we fill it up with ETH someone doesn’t steal our private key.

If the Sequencer is accruing fees that represent our revenue, we may need to add a way to transfer ETH out of the account.

The deployer node (in docker compose) uses only the DEPLOYER_PRIVATE_KEY all other PKs are marely used to pull the address:

ovmSequencerAddress: sequencer.address
ovmProposerAddress: proposer.address
ovmRelayerAddress: relayer.address

OVM_StateCommitmentChain.sol appendStateBatch/2 vault write -output-curl-string immutability-eth-plugin/wallets/sequencer/accounts/$ORIGINAL_AUTHORITY/ovm/appendStateBatch l1=true nonce=1 gas_price=$GAS_PRICE_HIGH batch=$BATCH shouldStartAtElement=$SHOULD_START_AT_ELEMENT contract=$OVM_SCC where the batch is an array of base64 encoded strings that we decode on the Vault side and transfer into 32 bytes array that fits into the contract API. value := [32]byte{} copy(key[:], []byte("hello"))

OVM_CanonicalTransactionChain.sol appendSequencerBatch/1 vault write -output-curl-string immutability-eth-plugin/wallets/sequencer/accounts/$ORIGINAL_AUTHORITY/ovm/appendSequencerBatch l1=true nonce=1 gas_price=$GAS_PRICE_HIGH batch=$BATCH contract=$OVM_CTC where the batch uses a custom encoding scheme for efficiency reasons on the contract side (calldata).

In TS this part looks like this:

const calldata = encodeAppendSequencerBatch(batch)
return OVM_CanonicalTransactionChain.signer.sendTransaction({
  to: OVM_CanonicalTransactionChain.address,
  data: '0x' + methodId + calldata,
})

and the encoding part:

export const encodeAppendSequencerBatch = (
  b: AppendSequencerBatchParams
): string => {
  const encodeShouldStartAtElement = encodeHex(b.shouldStartAtElement, 10)
  const encodedTotalElementsToAppend = encodeHex(b.totalElementsToAppend, 6)

  const encodedContextsHeader = encodeHex(b.contexts.length, 6)
  const encodedContexts =
    encodedContextsHeader +
    b.contexts.reduce((acc, cur) => acc + encodeBatchContext(cur), '')

  const encodedTransactionData = b.transactions.reduce((acc, cur) => {
    if (cur.length % 2 !== 0) {
      throw new Error('Unexpected uneven hex string value!')
    }
    const encodedTxDataHeader = remove0x(
      BigNumber.from(remove0x(cur).length / 2).toHexString()
    ).padStart(6, '0')
    return acc + encodedTxDataHeader + remove0x(cur)
  }, '')
  return (
    encodeShouldStartAtElement +
    encodedTotalElementsToAppend +
    encodedContexts +
    encodedTransactionData
  )
}

So in my opinion, I would leave this schema on the sequencer side and let the Vault marely decode base64 and send it through to the contract. We can do the schema validation on the Vault side (the same that happens on the contract side) so that we prevent arbitrary contract invocation.

OVM_CanonicalTransactionChain.sol appendQueueBatch/1 (body commented out) vault write -output-curl-string immutability-eth-plugin/wallets/sequencer/accounts/$ORIGINAL_AUTHORITY/ovm/appendQueueBatch l1=true nonce=1 gas_price=$GAS_PRICE_HIGH num_queued_transactions=$NUM_QUEUED_TRANSACTIONS contract=$OVM_CTC numQueuedTransactions is an integer

OVM_L1CrossDomainMessenger.sol relayMessage/5 and OVM_L1CrossDomainMessengerFast.sol relayMessage/5 use the same Solidity API:

function relayMessage(
  address _target,
  address _sender,
  bytes memory _message,
  uint256 _messageNonce,
  L2MessageInclusionProof memory _proof
)

vault write -output-curl-string immutability-eth-plugin/wallets/sequencer/accounts/$ORIGINAL_AUTHORITY/ovm/relayMessage l1=true nonce=1 gas_price=$GAS_PRICE_HIGH target=$TARGET sender=$SENDER message=$MESSAGE message_nonce=$MESSAGE_NONCE proof=$PROOF contract=$OVM_L1CDM where the proof is just a simple object (example below), message nonce is an integer and everything else a base64 integer string:

const proof1 = {
  stateRoot: ethers.constants.HashZero,
  stateRootBatchHeader: DUMMY_BATCH_HEADERS[0],
  stateRootProof: DUMMY_BATCH_PROOFS[0],
  stateTrieWitness: '0x',
  storageTrieWitness: '0x',
}

OVM_L2CrossDomainMessenger.sol relayMessage/4 with Solidity API

function relayMessage(
  address _target,
  address _sender,
  bytes memory _message,
  uint256 _messageNonce
)

vault write -output-curl-string immutability-eth-plugin/wallets/sequencer/accounts/$ORIGINAL_AUTHORITY/ovm/relayMessage l1=false nonce=1 gas_price=$GAS_PRICE_HIGH target=$TARGET sender=$SENDER message=$MESSAGE message_nonce=$MESSAGE_NONCE contract=$OVM_L1CDM same as above

As the Vault could interact both with L1 and L2, the configuration part would need to be expanded so that it supports talking to nodes on l1 and on l2.

vault write immutability-eth-plugin/config rpc_l1_url="$RPC_URL" chain_id="$CHAIN_ID" rpc_l2_url="$RPC_L2_URL" chain_l2_id="$CHAIN_ID_l2"
InoMurko commented 3 years ago

Thanks @kevsul for the first review.