bridge_transactions.go 8.77 KB
Newer Older
1 2 3 4 5
package database

import (
	"errors"
	"fmt"
Hamdi Allam's avatar
Hamdi Allam committed
6
	"math/big"
7 8 9

	"github.com/google/uuid"
	"gorm.io/gorm"
Hamdi Allam's avatar
Hamdi Allam committed
10
	"gorm.io/gorm/clause"
11 12

	"github.com/ethereum/go-ethereum/common"
Hamdi Allam's avatar
Hamdi Allam committed
13
	"github.com/ethereum/go-ethereum/log"
14 15 16 17 18 19 20
)

/**
 * Types
 */

type Transaction struct {
Hamdi Allam's avatar
Hamdi Allam committed
21 22
	FromAddress common.Address `gorm:"serializer:bytes"`
	ToAddress   common.Address `gorm:"serializer:bytes"`
Hamdi Allam's avatar
Hamdi Allam committed
23 24
	Amount      *big.Int       `gorm:"serializer:u256"`
	Data        Bytes          `gorm:"serializer:bytes"`
25 26 27 28
	Timestamp   uint64
}

type L1TransactionDeposit struct {
Hamdi Allam's avatar
Hamdi Allam committed
29 30
	SourceHash           common.Hash `gorm:"serializer:bytes;primaryKey"`
	L2TransactionHash    common.Hash `gorm:"serializer:bytes"`
31 32 33
	InitiatedL1EventGUID uuid.UUID

	Tx       Transaction `gorm:"embedded"`
Hamdi Allam's avatar
Hamdi Allam committed
34
	GasLimit *big.Int    `gorm:"serializer:u256"`
35 36 37
}

type L2TransactionWithdrawal struct {
Hamdi Allam's avatar
Hamdi Allam committed
38
	WithdrawalHash       common.Hash `gorm:"serializer:bytes;primaryKey"`
Hamdi Allam's avatar
Hamdi Allam committed
39
	Nonce                *big.Int    `gorm:"serializer:u256"`
40 41 42 43
	InitiatedL2EventGUID uuid.UUID

	ProvenL1EventGUID    *uuid.UUID
	FinalizedL1EventGUID *uuid.UUID
44
	Succeeded            *bool
45 46

	Tx       Transaction `gorm:"embedded"`
Hamdi Allam's avatar
Hamdi Allam committed
47
	GasLimit *big.Int    `gorm:"serializer:u256"`
48 49 50 51
}

type BridgeTransactionsView interface {
	L1TransactionDeposit(common.Hash) (*L1TransactionDeposit, error)
52
	L1LatestBlockHeader() (*L1BlockHeader, error)
53
	L1LatestFinalizedBlockHeader() (*L1BlockHeader, error)
54

55
	L2TransactionWithdrawal(common.Hash) (*L2TransactionWithdrawal, error)
56
	L2LatestBlockHeader() (*L2BlockHeader, error)
57
	L2LatestFinalizedBlockHeader() (*L2BlockHeader, error)
58 59 60 61 62
}

type BridgeTransactionsDB interface {
	BridgeTransactionsView

63
	StoreL1TransactionDeposits([]L1TransactionDeposit) error
64

65
	StoreL2TransactionWithdrawals([]L2TransactionWithdrawal) error
66
	MarkL2TransactionWithdrawalProvenEvent(common.Hash, uuid.UUID) error
67
	MarkL2TransactionWithdrawalFinalizedEvent(common.Hash, uuid.UUID, bool) error
68 69 70 71 72 73 74
}

/**
 * Implementation
 */

type bridgeTransactionsDB struct {
Hamdi Allam's avatar
Hamdi Allam committed
75
	log  log.Logger
76 77 78
	gorm *gorm.DB
}

Hamdi Allam's avatar
Hamdi Allam committed
79
func newBridgeTransactionsDB(log log.Logger, db *gorm.DB) BridgeTransactionsDB {
Hamdi Allam's avatar
Hamdi Allam committed
80
	return &bridgeTransactionsDB{log: log.New("table", "bridge_transactions"), gorm: db}
81 82 83 84 85 86
}

/**
 * Transactions deposited from L1
 */

87
func (db *bridgeTransactionsDB) StoreL1TransactionDeposits(deposits []L1TransactionDeposit) error {
Hamdi Allam's avatar
Hamdi Allam committed
88
	deduped := db.gorm.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "source_hash"}}, DoNothing: true})
Hamdi Allam's avatar
Hamdi Allam committed
89
	result := deduped.Create(&deposits)
90
	if result.Error == nil && int(result.RowsAffected) < len(deposits) {
Hamdi Allam's avatar
Hamdi Allam committed
91 92 93
		db.log.Warn("ignored L1 tx deposit duplicates", "duplicates", len(deposits)-int(result.RowsAffected))
	}

94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
	return result.Error
}

func (db *bridgeTransactionsDB) L1TransactionDeposit(sourceHash common.Hash) (*L1TransactionDeposit, error) {
	var deposit L1TransactionDeposit
	result := db.gorm.Where(&L1TransactionDeposit{SourceHash: sourceHash}).Take(&deposit)
	if result.Error != nil {
		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
			return nil, nil
		}
		return nil, result.Error
	}

	return &deposit, nil
}

110
func (db *bridgeTransactionsDB) L1LatestBlockHeader() (*L1BlockHeader, error) {
111 112
	// L1: Latest Transaction Deposit
	l1Query := db.gorm.Where("timestamp = (?)", db.gorm.Table("l1_transaction_deposits").Select("MAX(timestamp)"))
113

114 115 116 117 118 119 120 121 122 123 124 125 126 127
	var l1Header L1BlockHeader
	result := l1Query.Take(&l1Header)
	if result.Error != nil {
		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
			return nil, nil
		}
		return nil, result.Error
	}

	return &l1Header, nil
}

func (db *bridgeTransactionsDB) L1LatestFinalizedBlockHeader() (*L1BlockHeader, error) {
	// A Proven, Finalized Event or Relayed Message
128

129 130
	latestProvenWithdrawal := db.gorm.Table("l2_transaction_withdrawals").Where("proven_l1_event_guid IS NOT NULL").Order("timestamp DESC").Limit(1)
	provenQuery := db.gorm.Table("l1_contract_events").Where("guid = (?)", latestProvenWithdrawal.Select("proven_l1_event_guid"))
131

132 133
	latestFinalizedWithdrawal := db.gorm.Table("l2_transaction_withdrawals").Where("finalized_l1_event_guid IS NOT NULL").Order("timestamp DESC").Limit(1)
	finalizedQuery := db.gorm.Table("l1_contract_events").Where("guid = (?)", latestFinalizedWithdrawal.Select("finalized_l1_event_guid"))
134

135 136 137 138 139
	latestRelayedWithdrawal := db.gorm.Table("l2_bridge_messages").Where("relayed_message_event_guid IS NOT NULL").Order("timestamp DESC").Limit(1)
	relayedQuery := db.gorm.Table("l1_contract_events").Where("guid = (?)", latestRelayedWithdrawal.Select("relayed_message_event_guid"))

	events := db.gorm.Table("((?) UNION (?) UNION (?)) AS events", provenQuery, finalizedQuery, relayedQuery)
	l1Query := db.gorm.Where("hash = (?)", events.Select("block_hash").Order("timestamp DESC").Limit(1))
140 141 142 143 144 145 146 147 148 149 150 151 152

	var l1Header L1BlockHeader
	result := l1Query.Take(&l1Header)
	if result.Error != nil {
		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
			return nil, nil
		}
		return nil, result.Error
	}

	return &l1Header, nil
}

153 154 155 156
/**
 * Transactions withdrawn from L2
 */

157
func (db *bridgeTransactionsDB) StoreL2TransactionWithdrawals(withdrawals []L2TransactionWithdrawal) error {
Hamdi Allam's avatar
Hamdi Allam committed
158
	deduped := db.gorm.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "withdrawal_hash"}}, DoNothing: true})
Hamdi Allam's avatar
Hamdi Allam committed
159
	result := deduped.Create(&withdrawals)
160 161
	if result.Error == nil && int(result.RowsAffected) < len(withdrawals) {
		db.log.Warn("ignored L2 tx withdrawal duplicates", "duplicates", len(withdrawals)-int(result.RowsAffected))
Hamdi Allam's avatar
Hamdi Allam committed
162 163
	}

164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
	return result.Error
}

func (db *bridgeTransactionsDB) L2TransactionWithdrawal(withdrawalHash common.Hash) (*L2TransactionWithdrawal, error) {
	var withdrawal L2TransactionWithdrawal
	result := db.gorm.Where(&L2TransactionWithdrawal{WithdrawalHash: withdrawalHash}).Take(&withdrawal)
	if result.Error != nil {
		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
			return nil, nil
		}
		return nil, result.Error
	}

	return &withdrawal, nil
}

// MarkL2TransactionWithdrawalProvenEvent links a withdrawn transaction with associated Prove action on L1.
func (db *bridgeTransactionsDB) MarkL2TransactionWithdrawalProvenEvent(withdrawalHash common.Hash, provenL1EventGuid uuid.UUID) error {
	withdrawal, err := db.L2TransactionWithdrawal(withdrawalHash)
	if err != nil {
		return err
Hamdi Allam's avatar
Hamdi Allam committed
185
	} else if withdrawal == nil {
186 187 188
		return fmt.Errorf("transaction withdrawal hash %s not found", withdrawalHash)
	}

Hamdi Allam's avatar
Hamdi Allam committed
189 190 191
	if withdrawal.ProvenL1EventGUID != nil && withdrawal.ProvenL1EventGUID.ID() == provenL1EventGuid.ID() {
		return nil
	} else if withdrawal.ProvenL1EventGUID != nil {
Hamdi Allam's avatar
Hamdi Allam committed
192
		return fmt.Errorf("proven withdrawal %s re-proven with a different event %s", withdrawalHash, provenL1EventGuid)
Hamdi Allam's avatar
Hamdi Allam committed
193 194
	}

195 196 197 198 199 200
	withdrawal.ProvenL1EventGUID = &provenL1EventGuid
	result := db.gorm.Save(&withdrawal)
	return result.Error
}

// MarkL2TransactionWithdrawalProvenEvent links a withdrawn transaction in its finalized state
201
func (db *bridgeTransactionsDB) MarkL2TransactionWithdrawalFinalizedEvent(withdrawalHash common.Hash, finalizedL1EventGuid uuid.UUID, succeeded bool) error {
202 203 204
	withdrawal, err := db.L2TransactionWithdrawal(withdrawalHash)
	if err != nil {
		return err
Hamdi Allam's avatar
Hamdi Allam committed
205
	} else if withdrawal == nil {
206
		return fmt.Errorf("transaction withdrawal hash %s not found", withdrawalHash)
Hamdi Allam's avatar
Hamdi Allam committed
207
	} else if withdrawal.ProvenL1EventGUID == nil {
208 209 210
		return fmt.Errorf("cannot mark unproven withdrawal hash %s as finalized", withdrawal.WithdrawalHash)
	}

Hamdi Allam's avatar
Hamdi Allam committed
211 212 213
	if withdrawal.FinalizedL1EventGUID != nil && withdrawal.FinalizedL1EventGUID.ID() == finalizedL1EventGuid.ID() {
		return nil
	} else if withdrawal.FinalizedL1EventGUID != nil {
Hamdi Allam's avatar
Hamdi Allam committed
214
		return fmt.Errorf("finalized withdrawal %s re-finalized with a different event %s", withdrawalHash, finalizedL1EventGuid)
Hamdi Allam's avatar
Hamdi Allam committed
215 216
	}

217
	withdrawal.FinalizedL1EventGUID = &finalizedL1EventGuid
218
	withdrawal.Succeeded = &succeeded
219 220 221
	result := db.gorm.Save(&withdrawal)
	return result.Error
}
222 223

func (db *bridgeTransactionsDB) L2LatestBlockHeader() (*L2BlockHeader, error) {
224 225
	// L2: Block With The Latest Withdrawal
	l2Query := db.gorm.Where("timestamp = (?)", db.gorm.Table("l2_transaction_withdrawals").Select("MAX(timestamp)"))
226

227 228 229 230 231
	var l2Header L2BlockHeader
	result := l2Query.Take(&l2Header)
	if result.Error != nil {
		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
			return nil, nil
232
		}
233
		return nil, result.Error
234 235
	}

236
	return &l2Header, nil
237
}
238 239 240

func (db *bridgeTransactionsDB) L2LatestFinalizedBlockHeader() (*L2BlockHeader, error) {
	// Only a Relayed message since we dont track L1 deposit inclusion status.
241 242 243 244
	latestRelayedDeposit := db.gorm.Table("l1_bridge_messages").Where("relayed_message_event_guid IS NOT NULL").Order("timestamp DESC").Limit(1)
	relayedQuery := db.gorm.Table("l2_contract_events").Where("guid = (?)", latestRelayedDeposit.Select("relayed_message_event_guid"))

	l2Query := db.gorm.Where("hash = (?)", relayedQuery.Select("block_hash"))
245 246

	var l2Header L2BlockHeader
247
	result := l2Query.Take(&l2Header)
248 249 250 251 252 253 254 255 256
	if result.Error != nil {
		if errors.Is(result.Error, gorm.ErrRecordNotFound) {
			return nil, nil
		}
		return nil, result.Error
	}

	return &l2Header, nil
}