github.com/ethersphere/bee/v2@v2.2.0/pkg/node/chain.go (about) 1 // Copyright 2021 The Swarm Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package node 6 7 import ( 8 "context" 9 "encoding/hex" 10 "errors" 11 "fmt" 12 "math/big" 13 "strings" 14 "time" 15 16 "github.com/ethereum/go-ethereum" 17 "github.com/ethereum/go-ethereum/common" 18 "github.com/ethereum/go-ethereum/core/types" 19 "github.com/ethereum/go-ethereum/ethclient" 20 "github.com/ethereum/go-ethereum/rpc" 21 "github.com/ethersphere/bee/v2/pkg/config" 22 "github.com/ethersphere/bee/v2/pkg/crypto" 23 "github.com/ethersphere/bee/v2/pkg/log" 24 "github.com/ethersphere/bee/v2/pkg/p2p/libp2p" 25 "github.com/ethersphere/bee/v2/pkg/postage/postagecontract" 26 "github.com/ethersphere/bee/v2/pkg/sctx" 27 "github.com/ethersphere/bee/v2/pkg/settlement" 28 "github.com/ethersphere/bee/v2/pkg/settlement/swap" 29 "github.com/ethersphere/bee/v2/pkg/settlement/swap/chequebook" 30 "github.com/ethersphere/bee/v2/pkg/settlement/swap/erc20" 31 "github.com/ethersphere/bee/v2/pkg/settlement/swap/priceoracle" 32 "github.com/ethersphere/bee/v2/pkg/settlement/swap/swapprotocol" 33 "github.com/ethersphere/bee/v2/pkg/storage" 34 "github.com/ethersphere/bee/v2/pkg/transaction" 35 "github.com/ethersphere/bee/v2/pkg/transaction/wrapped" 36 "github.com/ethersphere/go-sw3-abi/sw3abi" 37 "github.com/prometheus/client_golang/prometheus" 38 ) 39 40 const ( 41 maxDelay = 1 * time.Minute 42 cancellationDepth = 12 43 additionalConfirmations = 2 44 ) 45 46 // InitChain will initialize the Ethereum backend at the given endpoint and 47 // set up the Transaction Service to interact with it using the provided signer. 48 func InitChain( 49 ctx context.Context, 50 logger log.Logger, 51 stateStore storage.StateStorer, 52 endpoint string, 53 oChainID int64, 54 signer crypto.Signer, 55 pollingInterval time.Duration, 56 chainEnabled bool, 57 ) (transaction.Backend, common.Address, int64, transaction.Monitor, transaction.Service, error) { 58 var backend transaction.Backend = &noOpChainBackend{ 59 chainID: oChainID, 60 } 61 62 if chainEnabled { 63 // connect to the real one 64 rpcClient, err := rpc.DialContext(ctx, endpoint) 65 if err != nil { 66 return nil, common.Address{}, 0, nil, nil, fmt.Errorf("dial blockchain client: %w", err) 67 } 68 69 var versionString string 70 err = rpcClient.CallContext(ctx, &versionString, "web3_clientVersion") 71 if err != nil { 72 logger.Info("could not connect to backend; in a swap-enabled network a working blockchain node (for xdai network in production, sepolia in testnet) is required; check your node or specify another node using --swap-endpoint.", "backend_endpoint", endpoint) 73 return nil, common.Address{}, 0, nil, nil, fmt.Errorf("blockchain client get version: %w", err) 74 } 75 76 logger.Info("connected to blockchain backend", "version", versionString) 77 78 backend = wrapped.NewBackend(ethclient.NewClient(rpcClient)) 79 } 80 81 chainID, err := backend.ChainID(ctx) 82 if err != nil { 83 return nil, common.Address{}, 0, nil, nil, fmt.Errorf("get chain id: %w", err) 84 } 85 86 overlayEthAddress, err := signer.EthereumAddress() 87 if err != nil { 88 return nil, common.Address{}, 0, nil, nil, fmt.Errorf("blockchain address: %w", err) 89 } 90 91 transactionMonitor := transaction.NewMonitor(logger, backend, overlayEthAddress, pollingInterval, cancellationDepth) 92 93 transactionService, err := transaction.NewService(logger, overlayEthAddress, backend, signer, stateStore, chainID, transactionMonitor) 94 if err != nil { 95 return nil, common.Address{}, 0, nil, nil, fmt.Errorf("new transaction service: %w", err) 96 } 97 98 return backend, overlayEthAddress, chainID.Int64(), transactionMonitor, transactionService, nil 99 } 100 101 // InitChequebookFactory will initialize the chequebook factory with the given 102 // chain backend. 103 func InitChequebookFactory(logger log.Logger, backend transaction.Backend, chainID int64, transactionService transaction.Service, factoryAddress string) (chequebook.Factory, error) { 104 var currentFactory common.Address 105 chainCfg, found := config.GetByChainID(chainID) 106 107 foundFactory := chainCfg.CurrentFactoryAddress 108 if factoryAddress == "" { 109 if !found { 110 return nil, fmt.Errorf("no known factory address for this network (chain id: %d)", chainID) 111 } 112 currentFactory = foundFactory 113 logger.Info("using default factory address", "chain_id", chainID, "factory_address", currentFactory) 114 } else if !common.IsHexAddress(factoryAddress) { 115 return nil, errors.New("malformed factory address") 116 } else { 117 currentFactory = common.HexToAddress(factoryAddress) 118 logger.Info("using custom factory address", "factory_address", currentFactory) 119 } 120 121 return chequebook.NewFactory(backend, transactionService, currentFactory), nil 122 } 123 124 // InitChequebookService will initialize the chequebook service with the given 125 // chequebook factory and chain backend. 126 func InitChequebookService( 127 ctx context.Context, 128 logger log.Logger, 129 stateStore storage.StateStorer, 130 signer crypto.Signer, 131 chainID int64, 132 backend transaction.Backend, 133 overlayEthAddress common.Address, 134 transactionService transaction.Service, 135 chequebookFactory chequebook.Factory, 136 initialDeposit string, 137 deployGasPrice string, 138 erc20Service erc20.Service, 139 ) (chequebook.Service, error) { 140 chequeSigner := chequebook.NewChequeSigner(signer, chainID) 141 142 deposit, ok := new(big.Int).SetString(initialDeposit, 10) 143 if !ok { 144 return nil, fmt.Errorf("initial swap deposit \"%s\" cannot be parsed", initialDeposit) 145 } 146 147 if deployGasPrice != "" { 148 gasPrice, ok := new(big.Int).SetString(deployGasPrice, 10) 149 if !ok { 150 return nil, fmt.Errorf("deploy gas price \"%s\" cannot be parsed", deployGasPrice) 151 } 152 ctx = sctx.SetGasPrice(ctx, gasPrice) 153 } 154 155 chequebookService, err := chequebook.Init( 156 ctx, 157 chequebookFactory, 158 stateStore, 159 logger, 160 deposit, 161 transactionService, 162 backend, 163 chainID, 164 overlayEthAddress, 165 chequeSigner, 166 erc20Service, 167 ) 168 if err != nil { 169 return nil, fmt.Errorf("chequebook init: %w", err) 170 } 171 172 return chequebookService, nil 173 } 174 175 func initChequeStoreCashout( 176 stateStore storage.StateStorer, 177 swapBackend transaction.Backend, 178 chequebookFactory chequebook.Factory, 179 chainID int64, 180 overlayEthAddress common.Address, 181 transactionService transaction.Service, 182 ) (chequebook.ChequeStore, chequebook.CashoutService) { 183 chequeStore := chequebook.NewChequeStore( 184 stateStore, 185 chequebookFactory, 186 chainID, 187 overlayEthAddress, 188 transactionService, 189 chequebook.RecoverCheque, 190 ) 191 192 cashout := chequebook.NewCashoutService( 193 stateStore, 194 swapBackend, 195 transactionService, 196 chequeStore, 197 ) 198 199 return chequeStore, cashout 200 } 201 202 // InitSwap will initialize and register the swap service. 203 func InitSwap( 204 p2ps *libp2p.Service, 205 logger log.Logger, 206 stateStore storage.StateStorer, 207 networkID uint64, 208 overlayEthAddress common.Address, 209 chequebookService chequebook.Service, 210 chequeStore chequebook.ChequeStore, 211 cashoutService chequebook.CashoutService, 212 accounting settlement.Accounting, 213 priceOracleAddress string, 214 chainID int64, 215 transactionService transaction.Service, 216 ) (*swap.Service, priceoracle.Service, error) { 217 218 var currentPriceOracleAddress common.Address 219 if priceOracleAddress == "" { 220 chainCfg, found := config.GetByChainID(chainID) 221 currentPriceOracleAddress = chainCfg.SwapPriceOracleAddress 222 if !found { 223 return nil, nil, errors.New("no known price oracle address for this network") 224 } 225 } else { 226 currentPriceOracleAddress = common.HexToAddress(priceOracleAddress) 227 } 228 229 priceOracle := priceoracle.New(logger, currentPriceOracleAddress, transactionService, 300) 230 priceOracle.Start() 231 swapProtocol := swapprotocol.New(p2ps, logger, overlayEthAddress, priceOracle) 232 swapAddressBook := swap.NewAddressbook(stateStore) 233 234 cashoutAddress := overlayEthAddress 235 if chequebookService != nil { 236 cashoutAddress = chequebookService.Address() 237 } 238 239 swapService := swap.New( 240 swapProtocol, 241 logger, 242 stateStore, 243 chequebookService, 244 chequeStore, 245 swapAddressBook, 246 networkID, 247 cashoutService, 248 accounting, 249 cashoutAddress, 250 ) 251 252 swapProtocol.SetSwap(swapService) 253 254 err := p2ps.AddProtocol(swapProtocol.Protocol()) 255 if err != nil { 256 return nil, nil, err 257 } 258 259 return swapService, priceOracle, nil 260 } 261 262 func GetTxHash(stateStore storage.StateStorer, logger log.Logger, trxString string) ([]byte, error) { 263 264 if trxString != "" { 265 txHashTrimmed := strings.TrimPrefix(trxString, "0x") 266 if len(txHashTrimmed) != 64 { 267 return nil, errors.New("invalid length") 268 } 269 txHash, err := hex.DecodeString(txHashTrimmed) 270 if err != nil { 271 return nil, err 272 } 273 logger.Info("using the provided transaction hash", "tx_hash", txHashTrimmed) 274 return txHash, nil 275 } 276 277 var txHash common.Hash 278 key := chequebook.ChequebookDeploymentKey 279 if err := stateStore.Get(key, &txHash); err != nil { 280 if errors.Is(err, storage.ErrNotFound) { 281 return nil, errors.New("chequebook deployment transaction hash not found, please specify the transaction hash manually") 282 } 283 return nil, err 284 } 285 286 logger.Info("using the chequebook transaction hash", "tx_hash", txHash) 287 return txHash.Bytes(), nil 288 } 289 290 func GetTxNextBlock(ctx context.Context, logger log.Logger, backend transaction.Backend, monitor transaction.Monitor, duration time.Duration, trx []byte, blockHash string) ([]byte, error) { 291 292 if blockHash != "" { 293 blockHashTrimmed := strings.TrimPrefix(blockHash, "0x") 294 if len(blockHashTrimmed) != 64 { 295 return nil, errors.New("invalid length") 296 } 297 blockHash, err := hex.DecodeString(blockHashTrimmed) 298 if err != nil { 299 return nil, err 300 } 301 logger.Info("using the provided block hash", "block_hash", hex.EncodeToString(blockHash)) 302 return blockHash, nil 303 } 304 305 block, err := transaction.WaitBlockAfterTransaction(ctx, backend, duration, common.BytesToHash(trx), additionalConfirmations) 306 if err != nil { 307 return nil, err 308 } 309 310 hash := block.Hash() 311 hashBytes := hash.Bytes() 312 313 logger.Info("using the next block hash from the blockchain", "block_hash", hex.EncodeToString(hashBytes)) 314 315 return hashBytes, nil 316 } 317 318 // noOpChequebookService is a noOp implementation for chequebook.Service interface. 319 type noOpChequebookService struct{} 320 321 func (m *noOpChequebookService) Deposit(context.Context, *big.Int) (hash common.Hash, err error) { 322 return hash, postagecontract.ErrChainDisabled 323 } 324 func (m *noOpChequebookService) Withdraw(context.Context, *big.Int) (hash common.Hash, err error) { 325 return hash, postagecontract.ErrChainDisabled 326 } 327 func (m *noOpChequebookService) WaitForDeposit(context.Context, common.Hash) error { 328 return postagecontract.ErrChainDisabled 329 } 330 func (m *noOpChequebookService) Balance(context.Context) (*big.Int, error) { 331 return nil, postagecontract.ErrChainDisabled 332 } 333 func (m *noOpChequebookService) AvailableBalance(context.Context) (*big.Int, error) { 334 return nil, postagecontract.ErrChainDisabled 335 } 336 func (m *noOpChequebookService) Address() common.Address { 337 return common.Address{} 338 } 339 func (m *noOpChequebookService) Issue(context.Context, common.Address, *big.Int, chequebook.SendChequeFunc) (*big.Int, error) { 340 return nil, postagecontract.ErrChainDisabled 341 } 342 func (m *noOpChequebookService) LastCheque(common.Address) (*chequebook.SignedCheque, error) { 343 return nil, postagecontract.ErrChainDisabled 344 } 345 func (m *noOpChequebookService) LastCheques() (map[common.Address]*chequebook.SignedCheque, error) { 346 return nil, postagecontract.ErrChainDisabled 347 } 348 349 // noOpChainBackend is a noOp implementation for transaction.Backend interface. 350 type noOpChainBackend struct { 351 chainID int64 352 } 353 354 func (m noOpChainBackend) Metrics() []prometheus.Collector { 355 return nil 356 } 357 358 func (m noOpChainBackend) CodeAt(context.Context, common.Address, *big.Int) ([]byte, error) { 359 return common.FromHex(sw3abi.SimpleSwapFactoryDeployedBinv0_6_5), nil 360 } 361 func (m noOpChainBackend) CallContract(context.Context, ethereum.CallMsg, *big.Int) ([]byte, error) { 362 return nil, errors.New("disabled chain backend") 363 } 364 func (m noOpChainBackend) HeaderByNumber(context.Context, *big.Int) (*types.Header, error) { 365 h := new(types.Header) 366 h.Time = uint64(time.Now().Unix()) 367 return h, nil 368 } 369 func (m noOpChainBackend) PendingNonceAt(context.Context, common.Address) (uint64, error) { 370 panic("chain no op: PendingNonceAt") 371 } 372 func (m noOpChainBackend) SuggestGasPrice(context.Context) (*big.Int, error) { 373 panic("chain no op: SuggestGasPrice") 374 } 375 func (m noOpChainBackend) SuggestGasTipCap(context.Context) (*big.Int, error) { 376 panic("chain no op: SuggestGasPrice") 377 } 378 func (m noOpChainBackend) EstimateGas(context.Context, ethereum.CallMsg) (uint64, error) { 379 panic("chain no op: EstimateGas") 380 } 381 func (m noOpChainBackend) SendTransaction(context.Context, *types.Transaction) error { 382 panic("chain no op: SendTransaction") 383 } 384 func (m noOpChainBackend) TransactionReceipt(context.Context, common.Hash) (*types.Receipt, error) { 385 r := new(types.Receipt) 386 r.BlockNumber = big.NewInt(1) 387 return r, nil 388 } 389 func (m noOpChainBackend) TransactionByHash(context.Context, common.Hash) (tx *types.Transaction, isPending bool, err error) { 390 panic("chain no op: TransactionByHash") 391 } 392 func (m noOpChainBackend) BlockNumber(context.Context) (uint64, error) { 393 return 4, nil 394 } 395 func (m noOpChainBackend) BalanceAt(context.Context, common.Address, *big.Int) (*big.Int, error) { 396 return nil, postagecontract.ErrChainDisabled 397 } 398 func (m noOpChainBackend) NonceAt(context.Context, common.Address, *big.Int) (uint64, error) { 399 panic("chain no op: NonceAt") 400 } 401 func (m noOpChainBackend) FilterLogs(context.Context, ethereum.FilterQuery) ([]types.Log, error) { 402 panic("chain no op: FilterLogs") 403 } 404 func (m noOpChainBackend) ChainID(context.Context) (*big.Int, error) { 405 return big.NewInt(m.chainID), nil 406 } 407 func (m noOpChainBackend) Close() {}