code.vegaprotocol.io/vega@v0.79.0/core/client/eth/ethereum_confirmations.go (about) 1 // Copyright (C) 2023 Gobalsky Labs Limited 2 // 3 // This program is free software: you can redistribute it and/or modify 4 // it under the terms of the GNU Affero General Public License as 5 // published by the Free Software Foundation, either version 3 of the 6 // License, or (at your option) any later version. 7 // 8 // This program is distributed in the hope that it will be useful, 9 // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 // GNU Affero General Public License for more details. 12 // 13 // You should have received a copy of the GNU Affero General Public License 14 // along with this program. If not, see <http://www.gnu.org/licenses/>. 15 16 package eth 17 18 import ( 19 "context" 20 "errors" 21 "math/big" 22 "sync" 23 "time" 24 25 ethtypes "github.com/ethereum/go-ethereum/core/types" 26 ) 27 28 var ( 29 ErrMissingConfirmations = errors.New("not enough confirmations") 30 ErrBlockNotFinalized = errors.New("block not finalized") 31 ) 32 33 type FinalityState int 34 35 const ( 36 FinalityStateSafe FinalityState = iota 37 FinalityStateFinalized 38 FinalityStateLatest 39 ) 40 41 //go:generate go run github.com/golang/mock/mockgen -destination mocks/ethereum_client_confirmations_mock.go -package mocks code.vegaprotocol.io/vega/core/staking EthereumClientConfirmations 42 type EthereumClientConfirmations interface { 43 HeaderByNumber(context.Context, *big.Int) (*ethtypes.Header, error) 44 } 45 46 //go:generate go run github.com/golang/mock/mockgen -destination mocks/time_mock.go -package mocks code.vegaprotocol.io/vega/core/client/eth Time 47 type Time interface { 48 Now() time.Time 49 } 50 51 type StdTime struct{} 52 53 func (StdTime) Now() time.Time { return time.Now() } 54 55 type EthereumConfirmations struct { 56 retryDelay time.Duration 57 58 ethClient EthereumClientConfirmations 59 60 time Time 61 62 mu sync.Mutex 63 required uint64 64 curHeight uint64 65 curHeightLastUpdate time.Time 66 finHeight uint64 67 finHeightLastUpdate time.Time 68 finState *big.Int 69 } 70 71 func NewEthereumConfirmations(cfg Config, ethClient EthereumClientConfirmations, time Time, cs FinalityState) *EthereumConfirmations { 72 if time == nil { 73 time = StdTime{} 74 } 75 76 conf := &EthereumConfirmations{ 77 retryDelay: cfg.RetryDelay.Get(), 78 ethClient: ethClient, 79 time: time, 80 } 81 82 switch cs { 83 case FinalityStateSafe: 84 conf.finState = big.NewInt(-4) 85 case FinalityStateFinalized: 86 conf.finState = big.NewInt(-3) 87 case FinalityStateLatest: 88 conf.finState = nil 89 default: 90 panic("unexpected confirmation state") 91 } 92 return conf 93 } 94 95 func (e *EthereumConfirmations) GetConfirmations() uint64 { 96 e.mu.Lock() 97 defer e.mu.Unlock() 98 return e.required 99 } 100 101 func (e *EthereumConfirmations) UpdateConfirmations(confirmations uint64) { 102 e.mu.Lock() 103 defer e.mu.Unlock() 104 e.required = confirmations 105 } 106 107 func (e *EthereumConfirmations) Check(block uint64) error { 108 if err := e.CheckRequiredConfirmations(block, e.required); err != nil { 109 return err 110 } 111 112 // if finality state is "latest" we do not need to check as this will already be done by the confirmations count 113 if e.finState == nil { 114 return nil 115 } 116 117 finalized, err := e.finalizedHeight(context.Background()) 118 if err != nil { 119 return err 120 } 121 122 if finalized < block { 123 return ErrBlockNotFinalized 124 } 125 126 return nil 127 } 128 129 func (e *EthereumConfirmations) CheckRequiredConfirmations(block uint64, required uint64) error { 130 curBlock, err := e.currentHeight(context.Background()) 131 if err != nil { 132 return err 133 } 134 135 if curBlock < block || (curBlock-block) < required { 136 return ErrMissingConfirmations 137 } 138 139 return nil 140 } 141 142 func (e *EthereumConfirmations) finalizedHeight(ctx context.Context) (uint64, error) { 143 e.mu.Lock() 144 defer e.mu.Unlock() 145 146 h, lastUpdate, err := e.getHeight(ctx, e.finHeight, e.finHeightLastUpdate, e.finState) 147 if err != nil { 148 return e.finHeight, err 149 } 150 151 // update cache 152 e.finHeightLastUpdate = lastUpdate 153 e.finHeight = h 154 return e.finHeight, err 155 } 156 157 func (e *EthereumConfirmations) currentHeight(ctx context.Context) (uint64, error) { 158 e.mu.Lock() 159 defer e.mu.Unlock() 160 161 h, lastUpdate, err := e.getHeight(ctx, e.curHeight, e.curHeightLastUpdate, nil) 162 if err != nil { 163 return e.curHeight, err 164 } 165 166 // update cache 167 e.curHeightLastUpdate = lastUpdate 168 e.curHeight = h 169 return e.curHeight, err 170 } 171 172 func (e *EthereumConfirmations) getHeight(ctx context.Context, lastHeight uint64, lastUpdate time.Time, block *big.Int) (uint64, time.Time, error) { 173 // if last update of the height was more that 15 seconds 174 // ago, we try to update, we assume an eth block takes 175 // ~15 seconds 176 ctx, cancel := context.WithTimeout(ctx, 5*time.Second) 177 defer cancel() 178 if now := e.time.Now(); lastUpdate.Add(e.retryDelay).Before(now) { 179 // get the last block header 180 h, err := e.ethClient.HeaderByNumber(ctx, block) 181 if err != nil { 182 return lastHeight, lastUpdate, err 183 } 184 lastUpdate = now 185 lastHeight = h.Number.Uint64() 186 } 187 188 return lastHeight, lastUpdate, nil 189 }