Commit beb842ae authored by Mark Tyneway's avatar Mark Tyneway

op-chain-ops: cross domain message

Implement encoding and hashing of `CrossDomainMessage`
structs. This includes some tests for both v0 and v1
that were generated using solidity.

This is required for the migration of withdrawal hashes.
parent d9ba4b27
package crossdomain
import (
"math/big"
"strings"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
)
var (
// NonceMask represents a mask used to extract version bytes from the nonce
NonceMask, _ = new(big.Int).SetString("0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16)
// relayMessage0ABI represents the v0 relay message encoding
relayMessage0ABI = "[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_target\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_sender\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"_message\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"_messageNonce\",\"type\":\"uint256\"}],\"name\":\"relayMessage\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}]"
// relayMessage1ABI represents the v1 relay message encoding
relayMessage1ABI = "[{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_nonce\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_sender\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"_target\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_value\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"_minGasLimit\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"_message\",\"type\":\"bytes\"}],\"name\":\"relayMessage\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"}]"
// relayMessage0 represents the ABI of relay message v0
relayMessage0 abi.ABI
// relayMessage1 represents the ABI of relay message v1
relayMessage1 abi.ABI
)
// Create the required ABIs
func init() {
var err error
relayMessage0, err = abi.JSON(strings.NewReader(relayMessage0ABI))
if err != nil {
panic(err)
}
relayMessage1, err = abi.JSON(strings.NewReader(relayMessage1ABI))
if err != nil {
panic(err)
}
}
// EncodeCrossDomainMessageV0 will encode the calldata for
// "relayMessage(address,address,bytes,uint256)",
func EncodeCrossDomainMessageV0(
target *common.Address,
sender *common.Address,
message []byte,
nonce *big.Int,
) ([]byte, error) {
return relayMessage0.Pack("relayMessage", target, sender, message, nonce)
}
// EncodeCrossDomainMessageV1 will encode the calldata for
// "relayMessage(uint256,address,address,uint256,uint256,bytes)",
func EncodeCrossDomainMessageV1(
nonce *big.Int,
sender *common.Address,
target *common.Address,
value *big.Int,
gasLimit *big.Int,
data []byte,
) ([]byte, error) {
return relayMessage1.Pack("relayMessage", nonce, sender, target, value, gasLimit, data)
}
// DecodeVersionedNonce will decode the version that is encoded in the nonce
func DecodeVersionedNonce(versioned *big.Int) (*big.Int, *big.Int) {
nonce := new(big.Int).And(versioned, NonceMask)
version := new(big.Int).Rsh(versioned, 240)
return nonce, version
}
// EncodeVersionedNonce will encode the version into the nonce
func EncodeVersionedNonce(nonce, version *big.Int) *big.Int {
shifted := new(big.Int).Lsh(version, 240)
return new(big.Int).Or(nonce, shifted)
}
package crossdomain_test
import (
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"github.com/stretchr/testify/require"
)
func FuzzVersionedNonce(f *testing.F) {
f.Fuzz(func(t *testing.T, _nonce []byte, _version uint16) {
inputNonce := new(big.Int).SetBytes(_nonce)
// Clamp nonce to uint240
if inputNonce.Cmp(crossdomain.NonceMask) > 0 {
inputNonce = new(big.Int).Set(crossdomain.NonceMask)
}
// Clamp version to 0 or 1
_version = _version % 2
inputVersion := new(big.Int).SetUint64(uint64(_version))
encodedNonce := crossdomain.EncodeVersionedNonce(inputNonce, inputVersion)
decodedNonce, decodedVersion := crossdomain.DecodeVersionedNonce(encodedNonce)
require.Equal(t, decodedNonce.Uint64(), inputNonce.Uint64())
require.Equal(t, decodedVersion.Uint64(), inputVersion.Uint64())
})
}
package crossdomain
import (
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)
// HashCrossDomainMessageV0 computes the pre bedrock cross domain messaging
// hashing scheme.
func HashCrossDomainMessageV0(
target *common.Address,
sender *common.Address,
data []byte,
nonce *big.Int,
) (common.Hash, error) {
encoded, err := EncodeCrossDomainMessageV0(target, sender, data, nonce)
if err != nil {
return common.Hash{}, err
}
hash := crypto.Keccak256(encoded)
return common.BytesToHash(hash), nil
}
// HashCrossDomainMessageV1 computes the first post bedrock cross domain
// messaging hashing scheme.
func HashCrossDomainMessageV1(
nonce *big.Int,
sender *common.Address,
target *common.Address,
value *big.Int,
gasLimit *big.Int,
data []byte,
) (common.Hash, error) {
encoded, err := EncodeCrossDomainMessageV1(nonce, sender, target, value, gasLimit, data)
if err != nil {
return common.Hash{}, err
}
hash := crypto.Keccak256(encoded)
return common.BytesToHash(hash), nil
}
package crossdomain
import (
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common"
)
// CrossDomainMessage represents a cross domain message
// used by the CrossDomainMessenger. The version is encoded
// in the nonce. Version 0 messages do not have a value,
// version 1 messages have a value and the most significant
// byte of the nonce is a 1
type CrossDomainMessage struct {
Nonce *big.Int
Sender *common.Address
Target *common.Address
Value *big.Int
GasLimit *big.Int
Data []byte
}
// NewCrossDomainMessage creates a CrossDomainMessage.
func NewCrossDomainMessage(
nonce *big.Int,
sender, target *common.Address,
value, gasLimit *big.Int,
data []byte,
) *CrossDomainMessage {
return &CrossDomainMessage{
Nonce: nonce,
Sender: sender,
Target: target,
Value: value,
GasLimit: gasLimit,
Data: data,
}
}
// Version will return the version of the CrossDomainMessage.
// It does this by looking at the first byte of the nonce.
func (c *CrossDomainMessage) Version() uint64 {
_, version := DecodeVersionedNonce(c.Nonce)
return version.Uint64()
}
// Encode will encode a cross domain message based on the version.
func (c *CrossDomainMessage) Encode() ([]byte, error) {
version := c.Version()
switch version {
case 0:
return EncodeCrossDomainMessageV0(c.Target, c.Sender, c.Data, c.Nonce)
case 1:
return EncodeCrossDomainMessageV1(c.Nonce, c.Sender, c.Target, c.Value, c.GasLimit, c.Data)
default:
return nil, fmt.Errorf("unknown nonce version %d", version)
}
}
// Hash will compute the hash of the CrossDomainMessage
func (c *CrossDomainMessage) Hash() (common.Hash, error) {
version := c.Version()
switch version {
case 0:
return HashCrossDomainMessageV0(c.Target, c.Sender, c.Data, c.Nonce)
case 1:
return HashCrossDomainMessageV1(c.Nonce, c.Sender, c.Target, c.Value, c.GasLimit, c.Data)
default:
return common.Hash{}, fmt.Errorf("unknown nonce version %d", version)
}
}
package crossdomain_test
import (
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/stretchr/testify/require"
)
// TestEncode tests the encoding of a CrossDomainMessage. The assertion was
// created using solidity.
func TestEncode(t *testing.T) {
t.Parallel()
t.Run("V0", func(t *testing.T) {
msg := crossdomain.NewCrossDomainMessage(
crossdomain.EncodeVersionedNonce(common.Big0, common.Big0),
&common.Address{},
&common.Address{19: 0x01},
big.NewInt(0),
big.NewInt(5),
[]byte{},
)
require.Equal(t, uint64(0), msg.Version())
encoded, err := msg.Encode()
require.Nil(t, err)
expect := hexutil.MustDecode("0xcbd4ece900000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
require.Equal(t, expect, encoded)
})
t.Run("V1", func(t *testing.T) {
msg := crossdomain.NewCrossDomainMessage(
crossdomain.EncodeVersionedNonce(common.Big1, common.Big1),
&common.Address{19: 0x01},
&common.Address{19: 0x02},
big.NewInt(100),
big.NewInt(555),
[]byte{},
)
require.Equal(t, uint64(1), msg.Version())
encoded, err := msg.Encode()
require.Nil(t, err)
expect := hexutil.MustDecode("0xd764ad0b0001000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000022b00000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000000")
require.Equal(t, expect, encoded)
})
}
// TestEncode tests the hash of a CrossDomainMessage. The assertion was
// created using solidity.
func TestHash(t *testing.T) {
t.Parallel()
t.Run("V0", func(t *testing.T) {
msg := crossdomain.NewCrossDomainMessage(
crossdomain.EncodeVersionedNonce(common.Big0, common.Big0),
&common.Address{},
&common.Address{19: 0x01},
big.NewInt(10),
big.NewInt(5),
[]byte{},
)
require.Equal(t, uint64(0), msg.Version())
hash, err := msg.Hash()
require.Nil(t, err)
expect := common.HexToHash("0x5bb579a193681e7c4d43c8c2e4bc6c2c447d21ef9fa887ca23b2d3f9a0fac065")
require.Equal(t, expect, hash)
})
t.Run("V1", func(t *testing.T) {
msg := crossdomain.NewCrossDomainMessage(
crossdomain.EncodeVersionedNonce(common.Big0, common.Big1),
&common.Address{},
&common.Address{19: 0x01},
big.NewInt(0),
big.NewInt(5),
[]byte{},
)
require.Equal(t, uint64(1), msg.Version())
hash, err := msg.Hash()
require.Nil(t, err)
expect := common.HexToHash("0x09bbda7f59cdaccab5c41cab4600bd458b2bd7d9f8410f13316fe07e5f4237cc")
require.Equal(t, expect, hash)
})
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment