decred.org/dcrdex@v1.0.5/server/asset/eth/eth.go (about) 1 // This code is available on the terms of the project LICENSE.md file, 2 // also available online at https://blueoakcouncil.org/license/1.0.0. 3 4 package eth 5 6 import ( 7 "bufio" 8 "bytes" 9 "context" 10 "crypto/sha256" 11 "errors" 12 "fmt" 13 "math/big" 14 "os" 15 "strconv" 16 "strings" 17 "sync" 18 "time" 19 20 "decred.org/dcrdex/dex" 21 "decred.org/dcrdex/dex/config" 22 dexeth "decred.org/dcrdex/dex/networks/eth" 23 "decred.org/dcrdex/server/asset" 24 "github.com/decred/dcrd/dcrec/secp256k1/v4" 25 "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa" 26 "github.com/ethereum/go-ethereum/common" 27 "github.com/ethereum/go-ethereum/core/types" 28 "github.com/ethereum/go-ethereum/crypto" 29 "github.com/ethereum/go-ethereum/params" 30 ) 31 32 type VersionedToken struct { 33 *dexeth.Token 34 Ver uint32 35 } 36 37 var registeredTokens = make(map[uint32]*VersionedToken) 38 39 func registerToken(assetID uint32, ver uint32) { 40 token, exists := dexeth.Tokens[assetID] 41 if !exists { 42 panic(fmt.Sprintf("no token constructor for asset ID %d", assetID)) 43 } 44 asset.RegisterToken(assetID, &TokenDriver{ 45 DriverBase: DriverBase{ 46 Ver: ver, 47 UI: token.UnitInfo, 48 Nam: token.Name, 49 }, 50 Token: token.Token, 51 }) 52 registeredTokens[assetID] = &VersionedToken{ 53 Token: token, 54 Ver: ver, 55 } 56 } 57 58 func init() { 59 asset.Register(BipID, &Driver{ 60 DriverBase: DriverBase{ 61 Ver: version, 62 UI: dexeth.UnitInfo, 63 Nam: "Ethereum", 64 }, 65 }) 66 67 registerToken(usdcID, 0) 68 registerToken(usdtID, 0) 69 registerToken(maticID, 0) 70 } 71 72 const ( 73 BipID = 60 74 ethContractVersion = 0 75 version = 0 76 ) 77 78 var ( 79 _ asset.Driver = (*Driver)(nil) 80 _ asset.TokenBacker = (*ETHBackend)(nil) 81 82 backendInfo = &asset.BackendInfo{ 83 SupportsDynamicTxFee: true, 84 } 85 86 usdcID, _ = dex.BipSymbolID("usdc.eth") 87 usdtID, _ = dex.BipSymbolID("usdt.eth") 88 maticID, _ = dex.BipSymbolID("matic.eth") 89 ) 90 91 func networkToken(vToken *VersionedToken, net dex.Network) (netToken *dexeth.NetToken, contract *dexeth.SwapContract, err error) { 92 netToken, found := vToken.NetTokens[net] 93 if !found { 94 return nil, nil, fmt.Errorf("no addresses for %s on %s", vToken.Name, net) 95 } 96 contract, found = netToken.SwapContracts[vToken.Ver] 97 if !found || contract.Address == (common.Address{}) { 98 return nil, nil, fmt.Errorf("no version %d address for %s on %s", vToken.Ver, vToken.Name, net) 99 } 100 return 101 } 102 103 type DriverBase struct { 104 UI dex.UnitInfo 105 Ver uint32 106 Nam string 107 } 108 109 // Version returns the Backend implementation's version number. 110 func (d *DriverBase) Version() uint32 { 111 return d.Ver 112 } 113 114 // DecodeCoinID creates a human-readable representation of a coin ID for 115 // Ethereum. This must be a transaction hash. 116 func (d *DriverBase) DecodeCoinID(coinID []byte) (string, error) { 117 txHash, err := dexeth.DecodeCoinID(coinID) 118 if err != nil { 119 return "", err 120 } 121 return txHash.String(), nil 122 } 123 124 // UnitInfo returns the dex.UnitInfo for the asset. 125 func (d *DriverBase) UnitInfo() dex.UnitInfo { 126 return d.UI 127 } 128 129 // Name is the asset's name. 130 func (d *DriverBase) Name() string { 131 return d.Nam 132 } 133 134 // Driver implements asset.Driver. 135 type Driver struct { 136 DriverBase 137 } 138 139 // Setup creates the ETH backend. Start the backend with its Run method. 140 func (d *Driver) Setup(cfg *asset.BackendConfig) (asset.Backend, error) { 141 var chainID uint64 142 switch cfg.Net { 143 case dex.Mainnet: 144 chainID = params.MainnetChainConfig.ChainID.Uint64() 145 case dex.Testnet: 146 chainID = params.SepoliaChainConfig.ChainID.Uint64() 147 default: 148 chainID = 42 149 } 150 151 return NewEVMBackend(cfg, chainID, dexeth.ContractAddresses, registeredTokens) 152 } 153 154 type TokenDriver struct { 155 DriverBase 156 Token *dex.Token 157 } 158 159 // TokenInfo returns details for a token asset. 160 func (d *TokenDriver) TokenInfo() *dex.Token { 161 return d.Token 162 } 163 164 // ethFetcher represents a blockchain information fetcher. In practice, it is 165 // satisfied by rpcclient. For testing, it can be satisfied by a stub. 166 // 167 // TODO: At some point multiple contracts will need to be used, at least for 168 // transitory periods when updating the contract, and possibly a random 169 // contract setup, and so contract addresses may need to be an argument in some 170 // of these methods. 171 type ethFetcher interface { 172 bestHeader(ctx context.Context) (*types.Header, error) 173 blockNumber(ctx context.Context) (uint64, error) 174 headerByHeight(ctx context.Context, height uint64) (*types.Header, error) 175 connect(ctx context.Context) error 176 suggestGasTipCap(ctx context.Context) (*big.Int, error) 177 transaction(ctx context.Context, hash common.Hash) (tx *types.Transaction, isMempool bool, err error) 178 // token- and asset-specific methods 179 loadToken(ctx context.Context, assetID uint32, vToken *VersionedToken) error 180 swap(ctx context.Context, assetID uint32, secretHash [32]byte) (*dexeth.SwapState, error) 181 accountBalance(ctx context.Context, assetID uint32, addr common.Address) (*big.Int, error) 182 } 183 184 type baseBackend struct { 185 // A connection-scoped Context is used to cancel active RPCs on 186 // connection shutdown. 187 ctx context.Context 188 net dex.Network 189 node ethFetcher 190 191 baseChainID uint32 192 baseChainName string 193 versionedTokens map[uint32]*VersionedToken 194 195 // bestHeight is the last best known chain tip height. bestHeight is set 196 // in Connect before the poll loop is started, and only updated in the poll 197 // loop thereafter. Do not use bestHeight outside of the poll loop unless 198 // you change it to an atomic. 199 bestHeight uint64 200 201 // A logger will be provided by the DEX. All logging should use the provided 202 // logger. 203 baseLogger dex.Logger 204 205 tokens map[uint32]*TokenBackend 206 } 207 208 // AssetBackend is an asset backend for Ethereum. It has methods for fetching output 209 // information and subscribing to block updates. 210 // AssetBackend implements asset.Backend, so provides exported methods for 211 // DEX-related blockchain info. 212 type AssetBackend struct { 213 *baseBackend 214 assetID uint32 215 log dex.Logger 216 atomize func(*big.Int) uint64 // atomize takes floor. use for values, not fee rates 217 218 // The backend provides block notification channels through the BlockChannel 219 // method. 220 blockChansMtx sync.RWMutex 221 blockChans map[chan *asset.BlockUpdate]struct{} 222 223 // initTxSize is the gas used for an initiation transaction with one swap. 224 initTxSize uint64 225 redeemSize uint64 226 227 contractAddr common.Address 228 } 229 230 // ETHBackend implements some Ethereum-specific methods. 231 type ETHBackend struct { 232 *AssetBackend 233 } 234 235 // TokenBackend implements some token-specific methods. 236 type TokenBackend struct { 237 *AssetBackend 238 *VersionedToken 239 } 240 241 // Check that Backend satisfies the Backend interface. 242 var _ asset.Backend = (*TokenBackend)(nil) 243 var _ asset.Backend = (*ETHBackend)(nil) 244 245 // Check that Backend satisfies the AccountBalancer interface. 246 var _ asset.AccountBalancer = (*TokenBackend)(nil) 247 var _ asset.AccountBalancer = (*ETHBackend)(nil) 248 249 // unconnectedETH returns a Backend without a node. The node should be set 250 // before use. 251 func unconnectedETH(bipID uint32, contractAddr common.Address, vTokens map[uint32]*VersionedToken, logger dex.Logger, net dex.Network) (*ETHBackend, error) { 252 // TODO: At some point multiple contracts will need to be used, at 253 // least for transitory periods when updating the contract, and 254 // possibly a random contract setup, and so this section will need to 255 // change to support multiple contracts. 256 return ÐBackend{&AssetBackend{ 257 baseBackend: &baseBackend{ 258 net: net, 259 baseLogger: logger, 260 tokens: make(map[uint32]*TokenBackend), 261 baseChainID: bipID, 262 baseChainName: strings.ToUpper(dex.BipIDSymbol(bipID)), 263 versionedTokens: vTokens, 264 }, 265 log: logger, 266 contractAddr: contractAddr, 267 blockChans: make(map[chan *asset.BlockUpdate]struct{}), 268 initTxSize: dexeth.InitGas(1, ethContractVersion), 269 redeemSize: dexeth.RedeemGas(1, ethContractVersion), 270 assetID: bipID, 271 atomize: dexeth.WeiToGwei, 272 }}, nil 273 } 274 275 func parseEndpoints(cfg *asset.BackendConfig) ([]endpoint, error) { 276 var endpoints []endpoint 277 if cfg.RelayAddr != "" { 278 endpoints = append(endpoints, endpoint{ 279 url: "http://" + cfg.RelayAddr, 280 priority: 10, 281 }) 282 } 283 file, err := os.Open(cfg.ConfigPath) 284 if err != nil { 285 if os.IsNotExist(err) && len(endpoints) > 0 { 286 return endpoints, nil 287 } 288 return nil, err 289 } 290 defer file.Close() 291 292 assetName := strings.ToUpper(dex.BipIDSymbol(cfg.AssetID)) 293 294 endpointsMap := make(map[string]bool) // to avoid duplicates 295 scanner := bufio.NewScanner(file) 296 for scanner.Scan() { 297 line := strings.TrimSpace(scanner.Text()) 298 if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") { 299 continue 300 } 301 ethCfgInstructions := "invalid %s config line: \"%s\". " + 302 "Each line must contain URL and optionally a priority (between 0-65535) " + 303 "separated by a comma. Example: \"https://www.infura.io/,2\"" 304 parts := strings.Split(line, ",") 305 if len(parts) < 1 || len(parts) > 2 { 306 return nil, fmt.Errorf(ethCfgInstructions, assetName, line) 307 } 308 309 url := strings.TrimSpace(parts[0]) 310 var priority uint16 311 if len(parts) == 2 { 312 priority64, err := strconv.ParseUint(strings.TrimSpace(parts[1]), 10, 16) 313 if err != nil { 314 return nil, fmt.Errorf(ethCfgInstructions, line) 315 } 316 priority = uint16(priority64) 317 } 318 if endpointsMap[url] { 319 continue 320 } 321 endpointsMap[line] = true 322 endpoints = append(endpoints, endpoint{ 323 url: url, 324 priority: priority, 325 }) 326 } 327 if err := scanner.Err(); err != nil { 328 return nil, fmt.Errorf("error reading %s config file at %q. %v", assetName, cfg.ConfigPath, err) 329 } 330 if len(endpoints) == 0 { 331 return nil, fmt.Errorf("no endpoint found in the %s config file at %q", assetName, cfg.ConfigPath) 332 } 333 334 return endpoints, nil 335 } 336 337 // NewEVMBackend is the exported constructor by which the DEX will import the 338 // Backend. 339 func NewEVMBackend( 340 cfg *asset.BackendConfig, 341 chainID uint64, 342 contractAddrs map[uint32]map[dex.Network]common.Address, 343 vTokens map[uint32]*VersionedToken, 344 ) (*ETHBackend, error) { 345 346 endpoints, err := parseEndpoints(cfg) 347 if err != nil { 348 return nil, err 349 } 350 351 baseChainID, net, log := cfg.AssetID, cfg.Net, cfg.Logger 352 assetName := strings.ToUpper(dex.BipIDSymbol(baseChainID)) 353 354 netAddrs, found := contractAddrs[ethContractVersion] 355 if !found { 356 return nil, fmt.Errorf("no contract address for %s version %d", assetName, ethContractVersion) 357 } 358 contractAddr, found := netAddrs[net] 359 if !found { 360 return nil, fmt.Errorf("no contract address for %s version %d on %s", assetName, ethContractVersion, net) 361 } 362 363 eth, err := unconnectedETH(baseChainID, contractAddr, vTokens, log, net) 364 if err != nil { 365 return nil, err 366 } 367 368 eth.node = newRPCClient(baseChainID, chainID, net, endpoints, contractAddr, log.SubLogger("RPC")) 369 return eth, nil 370 } 371 372 // Connect connects to the node RPC server and initializes some variables. 373 func (eth *ETHBackend) Connect(ctx context.Context) (*sync.WaitGroup, error) { 374 eth.baseBackend.ctx = ctx 375 376 // Create a separate context for the node so that it will only be cancelled 377 // after the ETHBackend's run method has returned. 378 nodeContext, cancelNodeContext := context.WithCancel(context.Background()) 379 if err := eth.node.connect(nodeContext); err != nil { 380 cancelNodeContext() 381 return nil, err 382 } 383 384 // Prime the best block hash and height. 385 bn, err := eth.node.blockNumber(ctx) 386 if err != nil { 387 cancelNodeContext() 388 return nil, fmt.Errorf("error getting best block header: %w", err) 389 } 390 eth.baseBackend.bestHeight = bn 391 392 var wg sync.WaitGroup 393 wg.Add(1) 394 go func() { 395 eth.run(ctx) 396 cancelNodeContext() 397 wg.Done() 398 }() 399 return &wg, nil 400 } 401 402 // Connect for TokenBackend just waits for context cancellation and closes the 403 // WaitGroup. 404 func (eth *TokenBackend) Connect(ctx context.Context) (*sync.WaitGroup, error) { 405 if eth.baseBackend.ctx == nil || eth.baseBackend.ctx.Err() != nil { 406 return nil, fmt.Errorf("parent asset not connected") 407 } 408 var wg sync.WaitGroup 409 wg.Add(1) 410 go func() { 411 defer wg.Done() 412 select { 413 case <-ctx.Done(): 414 case <-eth.baseBackend.ctx.Done(): 415 } 416 }() 417 return &wg, nil 418 } 419 420 // TokenBackend creates an *AssetBackend for a token. Part of the 421 // asset.TokenBacker interface. Do not call TokenBackend concurrently for the 422 // same asset. 423 func (eth *ETHBackend) TokenBackend(assetID uint32, configPath string) (asset.Backend, error) { 424 if _, found := eth.baseBackend.tokens[assetID]; found { 425 return nil, fmt.Errorf("asset %d backend already loaded", assetID) 426 } 427 428 vToken, found := eth.versionedTokens[assetID] 429 if !found { 430 return nil, fmt.Errorf("no token for asset ID %d", assetID) 431 } 432 433 _, swapContract, err := networkToken(vToken, eth.net) 434 if err != nil { 435 return nil, err 436 } 437 438 gases := new(configuredTokenGases) 439 if configPath != "" { 440 if err := config.ParseInto(configPath, gases); err != nil { 441 return nil, fmt.Errorf("error parsing fee overrides for token %d: %v", assetID, err) 442 } 443 } 444 445 if gases.Swap == 0 { 446 gases.Swap = swapContract.Gas.Swap 447 } 448 if gases.Redeem == 0 { 449 gases.Redeem = swapContract.Gas.Redeem 450 } 451 452 if err := eth.node.loadToken(eth.ctx, assetID, vToken); err != nil { 453 return nil, fmt.Errorf("error loading token for asset ID %d: %w", assetID, err) 454 } 455 be := &TokenBackend{ 456 AssetBackend: &AssetBackend{ 457 baseBackend: eth.baseBackend, 458 log: eth.baseLogger.SubLogger(strings.ToUpper(dex.BipIDSymbol(assetID))), 459 assetID: assetID, 460 blockChans: make(map[chan *asset.BlockUpdate]struct{}), 461 initTxSize: gases.Swap, 462 redeemSize: gases.Redeem, 463 contractAddr: swapContract.Address, 464 atomize: vToken.EVMToAtomic, 465 }, 466 VersionedToken: vToken, 467 } 468 eth.baseBackend.tokens[assetID] = be 469 return be, nil 470 } 471 472 // TxData fetches the raw transaction data. 473 func (eth *baseBackend) TxData(coinID []byte) ([]byte, error) { 474 txHash, err := dexeth.DecodeCoinID(coinID) 475 if err != nil { 476 return nil, fmt.Errorf("coin ID decoding error: %v", err) 477 } 478 479 tx, _, err := eth.node.transaction(eth.ctx, txHash) 480 if err != nil { 481 return nil, fmt.Errorf("error retrieving transaction: %w", err) 482 } 483 if tx == nil { // Possible? 484 return nil, fmt.Errorf("no transaction %s", txHash) 485 } 486 487 return tx.MarshalBinary() 488 } 489 490 // InitTxSize is an upper limit on the gas used for an initiation. 491 func (be *AssetBackend) InitTxSize() uint64 { 492 return be.initTxSize 493 } 494 495 // RedeemSize is the same as (dex.Asset).RedeemSize for the asset. 496 func (be *AssetBackend) RedeemSize() uint64 { 497 return be.redeemSize 498 } 499 500 // FeeRate returns the current optimal fee rate in gwei / gas. 501 func (eth *baseBackend) FeeRate(ctx context.Context) (uint64, error) { 502 hdr, err := eth.node.bestHeader(ctx) 503 if err != nil { 504 return 0, fmt.Errorf("error getting best header: %w", err) 505 } 506 507 if hdr.BaseFee == nil { 508 return 0, fmt.Errorf("%v block header does not contain base fee", eth.baseChainName) 509 } 510 511 suggestedGasTipCap, err := eth.node.suggestGasTipCap(ctx) 512 if err != nil { 513 return 0, fmt.Errorf("error getting suggested gas tip cap: %w", err) 514 } 515 516 feeRate := new(big.Int).Add( 517 suggestedGasTipCap, 518 new(big.Int).Mul(hdr.BaseFee, big.NewInt(2))) 519 520 feeRateGwei, err := dexeth.WeiToGweiSafe(feeRate) 521 if err != nil { 522 return 0, fmt.Errorf("failed to convert wei to gwei: %w", err) 523 } 524 525 return feeRateGwei, nil 526 } 527 528 // Info provides some general information about the backend. 529 func (*baseBackend) Info() *asset.BackendInfo { 530 return backendInfo 531 } 532 533 // ValidateFeeRate checks that the transaction fees used to initiate the 534 // contract are sufficient. For most assets only the contract.FeeRate() cannot 535 // be less than reqFeeRate, but for Eth, the gasTipCap must also be checked. 536 func (eth *baseBackend) ValidateFeeRate(coin asset.Coin, reqFeeRate uint64) bool { 537 sc, ok := coin.(*swapCoin) 538 if !ok { 539 eth.baseLogger.Error("%v contract coin type must be a swapCoin but got %T", eth.baseChainName, sc) 540 return false 541 } 542 543 // Legacy transactions are also supported. In a legacy transaction, the 544 // gas tip cap will be equal to the gas price. 545 if dexeth.WeiToGwei(sc.gasTipCap) < dexeth.MinGasTipCap { 546 sc.backend.log.Errorf("Transaction %s tip cap %d < %d", dexeth.WeiToGwei(sc.gasTipCap), dexeth.MinGasTipCap) 547 return false 548 } 549 550 if sc.gasFeeCap.Cmp(dexeth.GweiToWei(reqFeeRate)) < 0 { 551 sc.backend.log.Errorf("Transaction %s gas fee cap too low. %s wei / gas < %s gwei / gas", sc.gasFeeCap, reqFeeRate) 552 return false 553 } 554 555 return true 556 } 557 558 // BlockChannel creates and returns a new channel on which to receive block 559 // updates. If the returned channel is ever blocking, there will be no error 560 // logged from the eth package. Part of the asset.Backend interface. 561 func (be *AssetBackend) BlockChannel(size int) <-chan *asset.BlockUpdate { 562 c := make(chan *asset.BlockUpdate, size) 563 be.blockChansMtx.Lock() 564 defer be.blockChansMtx.Unlock() 565 be.blockChans[c] = struct{}{} 566 return c 567 } 568 569 // sendBlockUpdate sends the BlockUpdate to all subscribers. 570 func (be *AssetBackend) sendBlockUpdate(u *asset.BlockUpdate) { 571 be.blockChansMtx.RLock() 572 defer be.blockChansMtx.RUnlock() 573 for c := range be.blockChans { 574 select { 575 case c <- u: 576 default: 577 be.log.Error("failed to send block update on blocking channel") 578 } 579 } 580 } 581 582 // ValidateContract ensures that contractData encodes both the expected contract 583 // version and a secret hash. 584 func (eth *ETHBackend) ValidateContract(contractData []byte) error { 585 ver, _, err := dexeth.DecodeContractData(contractData) 586 if err != nil { // ensures secretHash is proper length 587 return err 588 } 589 590 if ver != version { 591 return fmt.Errorf("incorrect swap contract version %d, wanted %d", ver, version) 592 } 593 return nil 594 } 595 596 // ValidateContract ensures that contractData encodes both the expected swap 597 // contract version and a secret hash. 598 func (eth *TokenBackend) ValidateContract(contractData []byte) error { 599 ver, _, err := dexeth.DecodeContractData(contractData) 600 if err != nil { // ensures secretHash is proper length 601 return err 602 } 603 _, _, err = networkToken(eth.VersionedToken, eth.net) 604 if err != nil { 605 return fmt.Errorf("error locating token: %v", err) 606 } 607 if ver != eth.VersionedToken.Ver { 608 return fmt.Errorf("incorrect token swap contract version %d, wanted %d", ver, version) 609 } 610 611 return nil 612 } 613 614 // Contract is part of the asset.Backend interface. The contractData bytes 615 // encodes both the contract version targeted and the secret hash. 616 func (be *AssetBackend) Contract(coinID, contractData []byte) (*asset.Contract, error) { 617 // newSwapCoin validates the contractData, extracting version, secret hash, 618 // counterparty address, and locktime. The supported version is enforced. 619 sc, err := be.newSwapCoin(coinID, contractData) 620 if err != nil { 621 return nil, fmt.Errorf("unable to create coiner: %w", err) 622 } 623 624 // Confirmations performs some extra swap status checks if the the tx is 625 // mined. For init coins, this uses the contract account's state (if it is 626 // mined) to verify the value, counterparty, and lock time. 627 _, err = sc.Confirmations(be.ctx) 628 if err != nil { 629 return nil, fmt.Errorf("unable to get confirmations: %v", err) 630 } 631 return &asset.Contract{ 632 Coin: sc, 633 SwapAddress: sc.init.Participant.String(), 634 ContractData: contractData, 635 SecretHash: sc.secretHash[:], 636 TxData: sc.serializedTx, 637 LockTime: sc.init.LockTime, 638 }, nil 639 } 640 641 // ValidateSecret checks that the secret satisfies the secret hash. 642 func (eth *baseBackend) ValidateSecret(secret, contractData []byte) bool { 643 _, secretHash, err := dexeth.DecodeContractData(contractData) 644 if err != nil { 645 return false 646 } 647 sh := sha256.Sum256(secret) 648 return bytes.Equal(sh[:], secretHash[:]) 649 } 650 651 // Synced is true if the blockchain is ready for action. 652 func (eth *baseBackend) Synced() (bool, error) { 653 bh, err := eth.node.bestHeader(eth.ctx) 654 if err != nil { 655 return false, err 656 } 657 // Time in the header is in seconds. 658 nowInSecs := time.Now().Unix() 659 timeDiff := nowInSecs - int64(bh.Time) 660 return timeDiff < dexeth.MaxBlockInterval, nil 661 } 662 663 // Redemption returns a coin that represents a contract redemption. redeemCoinID 664 // should be the transaction that sent a redemption, while contractCoinID is the 665 // swap contract this redemption redeems. 666 func (be *AssetBackend) Redemption(redeemCoinID, _, contractData []byte) (asset.Coin, error) { 667 // newRedeemCoin uses the contract account's state to validate the 668 // contractData, extracting version, secret, and secret hash. The supported 669 // version is enforced. 670 rc, err := be.newRedeemCoin(redeemCoinID, contractData) 671 if err != nil { 672 return nil, fmt.Errorf("unable to create coiner: %w", err) 673 } 674 675 // Confirmations performs some extra swap status checks if the the tx 676 // is mined. For redeem coins, this is just a swap state check. 677 _, err = rc.Confirmations(be.ctx) 678 if err != nil { 679 return nil, fmt.Errorf("unable to get confirmations: %v", err) 680 } 681 return rc, nil 682 } 683 684 // ValidateCoinID attempts to decode the coinID. 685 func (eth *baseBackend) ValidateCoinID(coinID []byte) (string, error) { 686 txHash, err := dexeth.DecodeCoinID(coinID) 687 if err != nil { 688 return "<invalid>", err 689 } 690 return txHash.String(), nil 691 } 692 693 // CheckSwapAddress checks that the given address is parseable. 694 func (eth *baseBackend) CheckSwapAddress(addr string) bool { 695 return common.IsHexAddress(addr) 696 } 697 698 // AccountBalance retrieves the current account balance, including the effects 699 // of known unmined transactions. 700 func (be *AssetBackend) AccountBalance(addrStr string) (uint64, error) { 701 bigBal, err := be.node.accountBalance(be.ctx, be.assetID, common.HexToAddress(addrStr)) 702 if err != nil { 703 return 0, fmt.Errorf("accountBalance error: %w", err) 704 } 705 return be.atomize(bigBal), nil 706 } 707 708 // ValidateSignature checks that the pubkey is correct for the address and 709 // that the signature shows ownership of the associated private key. 710 func (eth *baseBackend) ValidateSignature(addr string, pubkey, msg, sig []byte) error { 711 const sigLen = 64 712 if len(sig) != sigLen { 713 return fmt.Errorf("expected sig length of %d bytes but got %d", sigLen, len(sig)) 714 } 715 ethPK, err := crypto.UnmarshalPubkey(pubkey) 716 if err != nil { 717 return fmt.Errorf("unable to unmarshal pubkey: %v", err) 718 } 719 // Ensure the pubkey matches the funding coin's account address. 720 // Internally, this will take the last twenty bytes of crypto.Keccak256 721 // of all but the first byte of the pubkey bytes and wrap it as a 722 // common.Address. 723 pubkeyAddr := crypto.PubkeyToAddress(*ethPK) 724 if addr != pubkeyAddr.String() { 725 return errors.New("pubkey does not correspond to address") 726 } 727 r := new(secp256k1.ModNScalar) 728 if overflow := r.SetByteSlice(sig[0:32]); overflow { 729 return errors.New("invalid signature: r >= group order") 730 } 731 s := new(secp256k1.ModNScalar) 732 if overflow := s.SetByteSlice(sig[32:64]); overflow { 733 return errors.New("invalid signature: s >= group order") 734 } 735 ecdsaSig := ecdsa.NewSignature(r, s) 736 737 pk, err := secp256k1.ParsePubKey(pubkey) 738 if err != nil { 739 return err 740 } 741 // Verify the signature. 742 if !ecdsaSig.Verify(crypto.Keccak256(msg), pk) { 743 return errors.New("cannot verify signature") 744 } 745 return nil 746 } 747 748 // poll pulls the best hash from an eth node and compares that to a stored 749 // hash. If the same does nothing. If different, updates the stored hash and 750 // notifies listeners on block chans. 751 func (eth *ETHBackend) poll(ctx context.Context) { 752 send := func(err error) { 753 if err != nil { 754 eth.log.Error(err) 755 } 756 u := &asset.BlockUpdate{ 757 Err: err, 758 } 759 760 eth.sendBlockUpdate(u) 761 762 for _, be := range eth.tokens { 763 be.sendBlockUpdate(u) 764 } 765 } 766 bn, err := eth.node.blockNumber(ctx) 767 if err != nil { 768 send(fmt.Errorf("error getting best block header: %w", err)) 769 return 770 } 771 if bn == eth.bestHeight { 772 // Same height, nothing to do. 773 return 774 } 775 eth.log.Debugf("Tip change from %d to %d.", eth.bestHeight, bn) 776 eth.bestHeight = bn 777 send(nil) 778 } 779 780 // run processes the queue and monitors the application context. 781 func (eth *ETHBackend) run(ctx context.Context) { 782 // Non-loopback providers are metered at 10 seconds internally to rpcclient, 783 // but loopback addresses allow more frequent checks. 784 blockPoll := time.NewTicker(time.Second) 785 defer blockPoll.Stop() 786 787 for { 788 select { 789 case <-blockPoll.C: 790 eth.poll(ctx) 791 case <-ctx.Done(): 792 return 793 } 794 } 795 }