utils.go 9.14 KB
Newer Older
1 2 3
package withdrawals

import (
4
	"bytes"
5 6 7 8 9 10 11 12 13 14 15 16 17
	"context"
	"errors"
	"fmt"
	"math/big"
	"time"

	"github.com/ethereum/go-ethereum/accounts/abi"
	"github.com/ethereum/go-ethereum/accounts/abi/bind"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/ethereum/go-ethereum/ethclient"
	"github.com/ethereum/go-ethereum/ethclient/gethclient"
18 19 20

	"github.com/ethereum-optimism/optimism/op-bindings/bindings"
	"github.com/ethereum-optimism/optimism/op-bindings/predeploys"
21 22
)

23
var MessagePassedTopic = crypto.Keccak256Hash([]byte("MessagePassed(uint256,address,address,uint256,uint256,bytes,bytes32)"))
24

25 26
// WaitForFinalizationPeriod waits until there is OutputProof for an L2 block number larger than the supplied l2BlockNumber
// and that the output is finalized.
27
// This functions polls and can block for a very long time if used on mainnet.
28 29 30
// This returns the block number to use for the proof generation.
func WaitForFinalizationPeriod(ctx context.Context, client *ethclient.Client, portalAddr common.Address, l2BlockNumber *big.Int) (uint64, error) {
	l2BlockNumber = new(big.Int).Set(l2BlockNumber) // Don't clobber caller owned l2BlockNumber
31 32
	opts := &bind.CallOpts{Context: ctx}

33
	portal, err := bindings.NewOptimismPortalCaller(portalAddr, client)
34 35 36 37 38 39 40
	if err != nil {
		return 0, err
	}
	l2OOAddress, err := portal.L2ORACLE(opts)
	if err != nil {
		return 0, err
	}
41
	l2OO, err := bindings.NewL2OutputOracleCaller(l2OOAddress, client)
42 43 44
	if err != nil {
		return 0, err
	}
45 46 47 48 49 50 51 52 53 54 55
	submissionInterval, err := l2OO.SUBMISSIONINTERVAL(opts)
	if err != nil {
		return 0, err
	}
	// Convert blockNumber to submission interval boundary
	rem := new(big.Int)
	l2BlockNumber, rem = l2BlockNumber.DivMod(l2BlockNumber, submissionInterval, rem)
	if rem.Cmp(common.Big0) != 0 {
		l2BlockNumber = l2BlockNumber.Add(l2BlockNumber, common.Big1)
	}
	l2BlockNumber = l2BlockNumber.Mul(l2BlockNumber, submissionInterval)
56

clabby's avatar
clabby committed
57
	finalizationPeriod, err := l2OO.FINALIZATIONPERIODSECONDS(opts)
58 59 60 61
	if err != nil {
		return 0, err
	}

62
	latest, err := l2OO.LatestBlockNumber(opts)
63 64 65 66
	if err != nil {
		return 0, err
	}

67
	// Now poll for the output to be submitted on chain
68
	var ticker *time.Ticker
69 70
	diff := new(big.Int).Sub(l2BlockNumber, latest)
	if diff.Cmp(big.NewInt(10)) > 0 {
71 72 73 74 75 76 77 78 79
		ticker = time.NewTicker(time.Minute)
	} else {
		ticker = time.NewTicker(time.Second)
	}

loop:
	for {
		select {
		case <-ticker.C:
80
			latest, err = l2OO.LatestBlockNumber(opts)
81 82 83
			if err != nil {
				return 0, err
			}
84 85
			// Already passed the submitted block (likely just equals rather than >= here).
			if latest.Cmp(l2BlockNumber) >= 0 {
86 87 88 89 90 91 92 93
				break loop
			}
		case <-ctx.Done():
			return 0, ctx.Err()
		}
	}

	// Now wait for it to be finalized
94
	output, err := l2OO.GetL2OutputAfter(opts, l2BlockNumber)
95 96 97
	if err != nil {
		return 0, err
	}
98 99 100
	if output.OutputRoot == [32]byte{} {
		return 0, errors.New("empty output root. likely no proposal at timestamp")
	}
101 102 103 104 105 106 107 108 109 110 111 112 113 114
	targetTimestamp := new(big.Int).Add(output.Timestamp, finalizationPeriod)
	targetTime := time.Unix(targetTimestamp.Int64(), 0)
	// Assume clock is relatively correct
	time.Sleep(time.Until(targetTime))
	// Poll for L1 Block to have a time greater than the target time
	ticker = time.NewTicker(time.Second)
	for {
		select {
		case <-ticker.C:
			header, err := client.HeaderByNumber(ctx, nil)
			if err != nil {
				return 0, err
			}
			if header.Time > targetTimestamp.Uint64() {
115
				return l2BlockNumber.Uint64(), nil
116 117 118 119 120 121 122 123 124 125 126 127
			}
		case <-ctx.Done():
			return 0, ctx.Err()
		}
	}

}

type ProofClient interface {
	GetProof(context.Context, common.Address, []string, *big.Int) (*gethclient.AccountResult, error)
}

128 129
type ReceiptClient interface {
	TransactionReceipt(context.Context, common.Hash) (*types.Receipt, error)
130 131
}

132 133 134
// ProvenWithdrawalParameters is the set of parameters to pass to the ProveWithdrawalTransaction
// and FinalizeWithdrawalTransaction functions
type ProvenWithdrawalParameters struct {
135 136 137 138 139
	Nonce           *big.Int
	Sender          common.Address
	Target          common.Address
	Value           *big.Int
	GasLimit        *big.Int
140
	L2OutputIndex   *big.Int
141
	Data            []byte
142
	OutputRootProof bindings.TypesOutputRootProof
143
	WithdrawalProof [][]byte // List of trie nodes to prove L2 storage
144 145
}

146
// ProveWithdrawalParameters queries L1 & L2 to generate all withdrawal parameters and proof necessary to prove a withdrawal on L1.
147
// The header provided is very important. It should be a block (timestamp) for which there is a submitted output in the L2 Output Oracle
148
// contract. If not, the withdrawal will fail as it the storage proof cannot be verified if there is no submitted state root.
149
func ProveWithdrawalParameters(ctx context.Context, proofCl ProofClient, l2ReceiptCl ReceiptClient, txHash common.Hash, header *types.Header, l2OutputOracleContract *bindings.L2OutputOracleCaller) (ProvenWithdrawalParameters, error) {
150
	// Transaction receipt
151
	receipt, err := l2ReceiptCl.TransactionReceipt(ctx, txHash)
152
	if err != nil {
153
		return ProvenWithdrawalParameters{}, err
154 155
	}
	// Parse the receipt
156
	ev, err := ParseMessagePassed(receipt)
157
	if err != nil {
158
		return ProvenWithdrawalParameters{}, err
159 160 161
	}
	// Generate then verify the withdrawal proof
	withdrawalHash, err := WithdrawalHash(ev)
162
	if !bytes.Equal(withdrawalHash[:], ev.WithdrawalHash[:]) {
163
		return ProvenWithdrawalParameters{}, errors.New("Computed withdrawal hash incorrectly")
164
	}
165
	if err != nil {
166
		return ProvenWithdrawalParameters{}, err
167 168
	}
	slot := StorageSlotOfWithdrawalHash(withdrawalHash)
169
	p, err := proofCl.GetProof(ctx, predeploys.L2ToL1MessagePasserAddr, []string{slot.String()}, header.Number)
170
	if err != nil {
171
		return ProvenWithdrawalParameters{}, err
172
	}
173 174 175 176 177 178

	// Fetch the L2OutputIndex from the L2 Output Oracle caller (on L1)
	l2OutputIndex, err := l2OutputOracleContract.GetL2OutputIndexAfter(&bind.CallOpts{}, header.Number)
	if err != nil {
		return ProvenWithdrawalParameters{}, fmt.Errorf("failed to get l2OutputIndex: %w", err)
	}
179 180 181
	// TODO: Could skip this step, but it's nice to double check it
	err = VerifyProof(header.Root, p)
	if err != nil {
182
		return ProvenWithdrawalParameters{}, err
183 184
	}
	if len(p.StorageProof) != 1 {
185
		return ProvenWithdrawalParameters{}, errors.New("invalid amount of storage proofs")
186 187 188 189 190 191 192 193
	}

	// Encode it as expected by the contract
	trieNodes := make([][]byte, len(p.StorageProof[0].Proof))
	for i, s := range p.StorageProof[0].Proof {
		trieNodes[i] = common.FromHex(s)
	}

194
	return ProvenWithdrawalParameters{
195 196 197 198 199 200 201
		Nonce:         ev.Nonce,
		Sender:        ev.Sender,
		Target:        ev.Target,
		Value:         ev.Value,
		GasLimit:      ev.GasLimit,
		L2OutputIndex: l2OutputIndex,
		Data:          ev.Data,
202
		OutputRootProof: bindings.TypesOutputRootProof{
203 204 205 206
			Version:                  [32]byte{}, // Empty for version 1
			StateRoot:                header.Root,
			MessagePasserStorageRoot: p.StorageHash,
			LatestBlockhash:          header.Hash(),
207
		},
208
		WithdrawalProof: trieNodes,
209 210 211 212 213 214 215 216 217 218
	}, nil
}

// Standard ABI types copied from golang ABI tests
var (
	Uint256Type, _ = abi.NewType("uint256", "", nil)
	BytesType, _   = abi.NewType("bytes", "", nil)
	AddressType, _ = abi.NewType("address", "", nil)
)

219 220
// WithdrawalHash computes the hash of the withdrawal that was stored in the L2toL1MessagePasser
// contract state.
221
// TODO:
222 223 224
//   - I don't like having to use the ABI Generated struct
//   - There should be a better way to run the ABI encoding
//   - These needs to be fuzzed against the solidity
225
func WithdrawalHash(ev *bindings.L2ToL1MessagePasserMessagePassed) (common.Hash, error) {
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241
	//  abi.encode(nonce, msg.sender, _target, msg.value, _gasLimit, _data)
	args := abi.Arguments{
		{Name: "nonce", Type: Uint256Type},
		{Name: "sender", Type: AddressType},
		{Name: "target", Type: AddressType},
		{Name: "value", Type: Uint256Type},
		{Name: "gasLimit", Type: Uint256Type},
		{Name: "data", Type: BytesType},
	}
	enc, err := args.Pack(ev.Nonce, ev.Sender, ev.Target, ev.Value, ev.GasLimit, ev.Data)
	if err != nil {
		return common.Hash{}, fmt.Errorf("failed to pack for withdrawal hash: %w", err)
	}
	return crypto.Keccak256Hash(enc), nil
}

242
// ParseMessagePassed parses MessagePassed events from
243 244
// a transaction receipt. It does not support multiple withdrawals
// per receipt.
245
func ParseMessagePassed(receipt *types.Receipt) (*bindings.L2ToL1MessagePasserMessagePassed, error) {
246
	contract, err := bindings.NewL2ToL1MessagePasser(common.Address{}, nil)
247 248 249
	if err != nil {
		return nil, err
	}
250 251

	for _, log := range receipt.Logs {
252
		if len(log.Topics) == 0 || log.Topics[0] != MessagePassedTopic {
253
			continue
254
		}
255

256
		ev, err := contract.ParseMessagePassed(*log)
257 258
		if err != nil {
			return nil, fmt.Errorf("failed to parse log: %w", err)
259
		}
260
		return ev, nil
261
	}
262
	return nil, errors.New("Unable to find MessagePassed event")
263 264
}

265
// StorageSlotOfWithdrawalHash determines the storage slot of the L2ToL1MessagePasser contract to look at
266 267
// given a WithdrawalHash
func StorageSlotOfWithdrawalHash(hash common.Hash) common.Hash {
268
	// The withdrawals mapping is the 0th storage slot in the L2ToL1MessagePasser contract.
269 270 271 272 273 274
	// To determine the storage slot, use keccak256(withdrawalHash ++ p)
	// Where p is the 32 byte value of the storage slot and ++ is concatenation
	buf := make([]byte, 64)
	copy(buf, hash[:])
	return crypto.Keccak256Hash(buf)
}