diff --git a/.gitignore b/.gitignore index 66fd13c9..95223572 100644 --- a/.gitignore +++ b/.gitignore @@ -7,9 +7,9 @@ # Test binary, built with `go test -c` *.test - +.idea # Output of the go coverage tool, specifically when used with LiteIDE *.out - +# vendor # Dependency directories (remove the comment below to include it) # vendor/ diff --git a/tokens/bridge/bridge.go b/tokens/bridge/bridge.go index 1976bb8f..50da72fe 100644 --- a/tokens/bridge/bridge.go +++ b/tokens/bridge/bridge.go @@ -14,6 +14,7 @@ import ( "github.com/anyswap/CrossChain-Bridge/tokens/eth" "github.com/anyswap/CrossChain-Bridge/tokens/fsn" "github.com/anyswap/CrossChain-Bridge/tokens/ltc" + "github.com/anyswap/CrossChain-Bridge/tokens/zol" ) // NewCrossChainBridge new bridge according to chain name @@ -32,6 +33,8 @@ func NewCrossChainBridge(id string, isSrc bool) tokens.CrossChainBridge { return eth.NewCrossChainBridge(isSrc) case strings.HasPrefix(blockChainIden, "FUSION"): return fsn.NewCrossChainBridge(isSrc) + case strings.HasPrefix(blockChainIden, "ZEROLIMIT"): + return zol.NewCrossChainBridge(isSrc) default: log.Fatalf("Unsupported block chain %v", id) return nil @@ -74,6 +77,8 @@ func InitCrossChainBridge(isServer bool) { ltc.Init(cfg.BtcExtra) case "BLOCK": block.Init(cfg.BtcExtra) + case "ZEROLIMIT": + zol.Init(cfg.BtcExtra) } dcrm.Init(cfg.Dcrm, isServer) diff --git a/tokens/zol/addr_test.go b/tokens/zol/addr_test.go new file mode 100644 index 00000000..b4febf5a --- /dev/null +++ b/tokens/zol/addr_test.go @@ -0,0 +1,21 @@ +package zol + +import ( + "fmt" + "github.com/anyswap/CrossChain-Bridge/tokens" + "testing" +) + +func TestDecodeAddress_SOL(t *testing.T) { + b := NewCrossChainBridge(true) + b.ChainConfig = &tokens.ChainConfig{ + BlockChain: "ZeroLimit", + NetID: "mainnet", + } + + res, err := b.DecodeAddress("3DGNfnbTYUgJ8B3Vwq7U5cF8baXq9Tp9AC") + fmt.Printf("%v, %v", res, err) + if err != nil { + panic("") + } +} diff --git a/tokens/zol/address.go b/tokens/zol/address.go new file mode 100644 index 00000000..2f477b71 --- /dev/null +++ b/tokens/zol/address.go @@ -0,0 +1,62 @@ +package zol + +import ( + "fmt" + + "github.com/btcsuite/btcutil" +) + +// DecodeAddress decode address +func (b *Bridge) DecodeAddress(addr string) (address btcutil.Address, err error) { + chainConfig := b.Inherit.GetChainParams() + address, err = btcutil.DecodeAddress(addr, chainConfig) + if err != nil { + return + } + if !address.IsForNet(chainConfig) { + err = fmt.Errorf("invalid address for net") + return + } + return +} + +// NewAddressPubKeyHash encap +func (b *Bridge) NewAddressPubKeyHash(pkData []byte) (*btcutil.AddressPubKeyHash, error) { + return btcutil.NewAddressPubKeyHash(btcutil.Hash160(pkData), b.Inherit.GetChainParams()) +} + +// NewAddressScriptHash encap +func (b *Bridge) NewAddressScriptHash(redeemScript []byte) (*btcutil.AddressScriptHash, error) { + return btcutil.NewAddressScriptHash(redeemScript, b.Inherit.GetChainParams()) +} + +// IsValidAddress check address +func (b *Bridge) IsValidAddress(addr string) bool { + _, err := b.DecodeAddress(addr) + return err == nil +} + +// IsP2pkhAddress check p2pkh addrss +func (b *Bridge) IsP2pkhAddress(addr string) bool { + address, err := b.DecodeAddress(addr) + if err != nil { + return false + } + _, ok := address.(*btcutil.AddressPubKeyHash) + return ok +} + +// IsP2shAddress check p2sh addrss +func (b *Bridge) IsP2shAddress(addr string) bool { + address, err := b.DecodeAddress(addr) + if err != nil { + return false + } + _, ok := address.(*btcutil.AddressScriptHash) + return ok +} + +// DecodeWIF decode wif +func DecodeWIF(wif string) (*btcutil.WIF, error) { + return btcutil.DecodeWIF(wif) +} diff --git a/tokens/zol/address_test.go b/tokens/zol/address_test.go new file mode 100644 index 00000000..529ed940 --- /dev/null +++ b/tokens/zol/address_test.go @@ -0,0 +1,892 @@ +// Copyright (c) 2013-2017 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package zol + +import ( + "bytes" + "encoding/hex" + "fmt" + "reflect" + "strings" + "testing" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "golang.org/x/crypto/ripemd160" +) + +type CustomParamStruct struct { + Net wire.BitcoinNet + PubKeyHashAddrID byte + ScriptHashAddrID byte + Bech32HRPSegwit string +} + +var CustomParams = CustomParamStruct{ + Net: 0xdbb6c0fb, // litecoin mainnet HD version bytes + PubKeyHashAddrID: 0x30, // starts with L + ScriptHashAddrID: 0x32, // starts with M + Bech32HRPSegwit: "ltc", // starts with ltc +} + +// We use this function to be able to test functionality in DecodeAddress for +// defaultNet addresses +func applyCustomParams(params chaincfg.Params, customParams CustomParamStruct) chaincfg.Params { + params.Net = customParams.Net + params.PubKeyHashAddrID = customParams.PubKeyHashAddrID + params.ScriptHashAddrID = customParams.ScriptHashAddrID + params.Bech32HRPSegwit = customParams.Bech32HRPSegwit + return params +} + +var customParams = applyCustomParams(chaincfg.MainNetParams, CustomParams) + +func TestAddresses(t *testing.T) { + tests := []struct { + name string + addr string + encoded string + valid bool + result btcutil.Address + f func() (btcutil.Address, error) + net *chaincfg.Params + }{ + // Positive P2PKH tests. + { + name: "mainnet p2pkh", + addr: "1MirQ9bwyQcGVJPwKUgapu5ouK2E2Ey4gX", + encoded: "1MirQ9bwyQcGVJPwKUgapu5ouK2E2Ey4gX", + valid: true, + result: btcutil.TstAddressPubKeyHash( + [ripemd160.Size]byte{ + 0xe3, 0x4c, 0xce, 0x70, 0xc8, 0x63, 0x73, 0x27, 0x3e, 0xfc, + 0xc5, 0x4c, 0xe7, 0xd2, 0xa4, 0x91, 0xbb, 0x4a, 0x0e, 0x84}, + chaincfg.MainNetParams.PubKeyHashAddrID), + f: func() (btcutil.Address, error) { + pkHash := []byte{ + 0xe3, 0x4c, 0xce, 0x70, 0xc8, 0x63, 0x73, 0x27, 0x3e, 0xfc, + 0xc5, 0x4c, 0xe7, 0xd2, 0xa4, 0x91, 0xbb, 0x4a, 0x0e, 0x84} + return btcutil.NewAddressPubKeyHash(pkHash, &chaincfg.MainNetParams) + }, + net: &chaincfg.MainNetParams, + }, + { + name: "mainnet p2pkh 2", + addr: "12MzCDwodF9G1e7jfwLXfR164RNtx4BRVG", + encoded: "12MzCDwodF9G1e7jfwLXfR164RNtx4BRVG", + valid: true, + result: btcutil.TstAddressPubKeyHash( + [ripemd160.Size]byte{ + 0x0e, 0xf0, 0x30, 0x10, 0x7f, 0xd2, 0x6e, 0x0b, 0x6b, 0xf4, + 0x05, 0x12, 0xbc, 0xa2, 0xce, 0xb1, 0xdd, 0x80, 0xad, 0xaa}, + chaincfg.MainNetParams.PubKeyHashAddrID), + f: func() (btcutil.Address, error) { + pkHash := []byte{ + 0x0e, 0xf0, 0x30, 0x10, 0x7f, 0xd2, 0x6e, 0x0b, 0x6b, 0xf4, + 0x05, 0x12, 0xbc, 0xa2, 0xce, 0xb1, 0xdd, 0x80, 0xad, 0xaa} + return btcutil.NewAddressPubKeyHash(pkHash, &chaincfg.MainNetParams) + }, + net: &chaincfg.MainNetParams, + }, + { + name: "litecoin mainnet p2pkh", + addr: "LM2WMpR1Rp6j3Sa59cMXMs1SPzj9eXpGc1", + encoded: "LM2WMpR1Rp6j3Sa59cMXMs1SPzj9eXpGc1", + valid: true, + result: btcutil.TstAddressPubKeyHash( + [ripemd160.Size]byte{ + 0x13, 0xc6, 0x0d, 0x8e, 0x68, 0xd7, 0x34, 0x9f, 0x5b, 0x4c, + 0xa3, 0x62, 0xc3, 0x95, 0x4b, 0x15, 0x04, 0x50, 0x61, 0xb1}, + CustomParams.PubKeyHashAddrID), + f: func() (btcutil.Address, error) { + pkHash := []byte{ + 0x13, 0xc6, 0x0d, 0x8e, 0x68, 0xd7, 0x34, 0x9f, 0x5b, 0x4c, + 0xa3, 0x62, 0xc3, 0x95, 0x4b, 0x15, 0x04, 0x50, 0x61, 0xb1} + return btcutil.NewAddressPubKeyHash(pkHash, &customParams) + }, + net: &customParams, + }, + { + name: "testnet p2pkh", + addr: "mrX9vMRYLfVy1BnZbc5gZjuyaqH3ZW2ZHz", + encoded: "mrX9vMRYLfVy1BnZbc5gZjuyaqH3ZW2ZHz", + valid: true, + result: btcutil.TstAddressPubKeyHash( + [ripemd160.Size]byte{ + 0x78, 0xb3, 0x16, 0xa0, 0x86, 0x47, 0xd5, 0xb7, 0x72, 0x83, + 0xe5, 0x12, 0xd3, 0x60, 0x3f, 0x1f, 0x1c, 0x8d, 0xe6, 0x8f}, + chaincfg.TestNet3Params.PubKeyHashAddrID), + f: func() (btcutil.Address, error) { + pkHash := []byte{ + 0x78, 0xb3, 0x16, 0xa0, 0x86, 0x47, 0xd5, 0xb7, 0x72, 0x83, + 0xe5, 0x12, 0xd3, 0x60, 0x3f, 0x1f, 0x1c, 0x8d, 0xe6, 0x8f} + return btcutil.NewAddressPubKeyHash(pkHash, &chaincfg.TestNet3Params) + }, + net: &chaincfg.TestNet3Params, + }, + + // Negative P2PKH tests. + { + name: "p2pkh wrong hash length", + addr: "", + valid: false, + f: func() (btcutil.Address, error) { + pkHash := []byte{ + 0x00, 0x0e, 0xf0, 0x30, 0x10, 0x7f, 0xd2, 0x6e, 0x0b, 0x6b, + 0xf4, 0x05, 0x12, 0xbc, 0xa2, 0xce, 0xb1, 0xdd, 0x80, 0xad, + 0xaa} + return btcutil.NewAddressPubKeyHash(pkHash, &chaincfg.MainNetParams) + }, + net: &chaincfg.MainNetParams, + }, + { + name: "p2pkh bad checksum", + addr: "1MirQ9bwyQcGVJPwKUgapu5ouK2E2Ey4gY", + valid: false, + net: &chaincfg.MainNetParams, + }, + + // Positive P2SH tests. + { + // Taken from transactions: + // output: 3c9018e8d5615c306d72397f8f5eef44308c98fb576a88e030c25456b4f3a7ac + // input: 837dea37ddc8b1e3ce646f1a656e79bbd8cc7f558ac56a169626d649ebe2a3ba. + name: "mainnet p2sh", + addr: "3QJmV3qfvL9SuYo34YihAf3sRCW3qSinyC", + encoded: "3QJmV3qfvL9SuYo34YihAf3sRCW3qSinyC", + valid: true, + result: btcutil.TstAddressScriptHash( + [ripemd160.Size]byte{ + 0xf8, 0x15, 0xb0, 0x36, 0xd9, 0xbb, 0xbc, 0xe5, 0xe9, 0xf2, + 0xa0, 0x0a, 0xbd, 0x1b, 0xf3, 0xdc, 0x91, 0xe9, 0x55, 0x10}, + chaincfg.MainNetParams.ScriptHashAddrID), + f: func() (btcutil.Address, error) { + script := []byte{ + 0x52, 0x41, 0x04, 0x91, 0xbb, 0xa2, 0x51, 0x09, 0x12, 0xa5, + 0xbd, 0x37, 0xda, 0x1f, 0xb5, 0xb1, 0x67, 0x30, 0x10, 0xe4, + 0x3d, 0x2c, 0x6d, 0x81, 0x2c, 0x51, 0x4e, 0x91, 0xbf, 0xa9, + 0xf2, 0xeb, 0x12, 0x9e, 0x1c, 0x18, 0x33, 0x29, 0xdb, 0x55, + 0xbd, 0x86, 0x8e, 0x20, 0x9a, 0xac, 0x2f, 0xbc, 0x02, 0xcb, + 0x33, 0xd9, 0x8f, 0xe7, 0x4b, 0xf2, 0x3f, 0x0c, 0x23, 0x5d, + 0x61, 0x26, 0xb1, 0xd8, 0x33, 0x4f, 0x86, 0x41, 0x04, 0x86, + 0x5c, 0x40, 0x29, 0x3a, 0x68, 0x0c, 0xb9, 0xc0, 0x20, 0xe7, + 0xb1, 0xe1, 0x06, 0xd8, 0xc1, 0x91, 0x6d, 0x3c, 0xef, 0x99, + 0xaa, 0x43, 0x1a, 0x56, 0xd2, 0x53, 0xe6, 0x92, 0x56, 0xda, + 0xc0, 0x9e, 0xf1, 0x22, 0xb1, 0xa9, 0x86, 0x81, 0x8a, 0x7c, + 0xb6, 0x24, 0x53, 0x2f, 0x06, 0x2c, 0x1d, 0x1f, 0x87, 0x22, + 0x08, 0x48, 0x61, 0xc5, 0xc3, 0x29, 0x1c, 0xcf, 0xfe, 0xf4, + 0xec, 0x68, 0x74, 0x41, 0x04, 0x8d, 0x24, 0x55, 0xd2, 0x40, + 0x3e, 0x08, 0x70, 0x8f, 0xc1, 0xf5, 0x56, 0x00, 0x2f, 0x1b, + 0x6c, 0xd8, 0x3f, 0x99, 0x2d, 0x08, 0x50, 0x97, 0xf9, 0x97, + 0x4a, 0xb0, 0x8a, 0x28, 0x83, 0x8f, 0x07, 0x89, 0x6f, 0xba, + 0xb0, 0x8f, 0x39, 0x49, 0x5e, 0x15, 0xfa, 0x6f, 0xad, 0x6e, + 0xdb, 0xfb, 0x1e, 0x75, 0x4e, 0x35, 0xfa, 0x1c, 0x78, 0x44, + 0xc4, 0x1f, 0x32, 0x2a, 0x18, 0x63, 0xd4, 0x62, 0x13, 0x53, + 0xae} + return btcutil.NewAddressScriptHash(script, &chaincfg.MainNetParams) + }, + net: &chaincfg.MainNetParams, + }, + { + name: "litecoin mainnet P2SH ", + addr: "MVcg9uEvtWuP5N6V48EHfEtbz48qR8TKZ9", + encoded: "MVcg9uEvtWuP5N6V48EHfEtbz48qR8TKZ9", + valid: true, + result: btcutil.TstAddressScriptHash( + [ripemd160.Size]byte{ + 0xee, 0x34, 0xac, 0x67, 0x6b, 0xda, 0xf6, 0xe3, 0x70, 0xc8, + 0xc8, 0x20, 0xb9, 0x48, 0xed, 0xfa, 0xd3, 0xa8, 0x73, 0xd8}, + CustomParams.ScriptHashAddrID), + f: func() (btcutil.Address, error) { + pkHash := []byte{ + 0xEE, 0x34, 0xAC, 0x67, 0x6B, 0xDA, 0xF6, 0xE3, 0x70, 0xC8, + 0xC8, 0x20, 0xB9, 0x48, 0xED, 0xFA, 0xD3, 0xA8, 0x73, 0xD8} + return btcutil.NewAddressScriptHashFromHash(pkHash, &customParams) + }, + net: &customParams, + }, + { + // Taken from transactions: + // output: b0539a45de13b3e0403909b8bd1a555b8cbe45fd4e3f3fda76f3a5f52835c29d + // input: (not yet redeemed at time test was written) + name: "mainnet p2sh 2", + addr: "3NukJ6fYZJ5Kk8bPjycAnruZkE5Q7UW7i8", + encoded: "3NukJ6fYZJ5Kk8bPjycAnruZkE5Q7UW7i8", + valid: true, + result: btcutil.TstAddressScriptHash( + [ripemd160.Size]byte{ + 0xe8, 0xc3, 0x00, 0xc8, 0x79, 0x86, 0xef, 0xa8, 0x4c, 0x37, + 0xc0, 0x51, 0x99, 0x29, 0x01, 0x9e, 0xf8, 0x6e, 0xb5, 0xb4}, + chaincfg.MainNetParams.ScriptHashAddrID), + f: func() (btcutil.Address, error) { + hash := []byte{ + 0xe8, 0xc3, 0x00, 0xc8, 0x79, 0x86, 0xef, 0xa8, 0x4c, 0x37, + 0xc0, 0x51, 0x99, 0x29, 0x01, 0x9e, 0xf8, 0x6e, 0xb5, 0xb4} + return btcutil.NewAddressScriptHashFromHash(hash, &chaincfg.MainNetParams) + }, + net: &chaincfg.MainNetParams, + }, + { + // Taken from bitcoind base58_keys_valid. + name: "testnet p2sh", + addr: "2NBFNJTktNa7GZusGbDbGKRZTxdK9VVez3n", + encoded: "2NBFNJTktNa7GZusGbDbGKRZTxdK9VVez3n", + valid: true, + result: btcutil.TstAddressScriptHash( + [ripemd160.Size]byte{ + 0xc5, 0x79, 0x34, 0x2c, 0x2c, 0x4c, 0x92, 0x20, 0x20, 0x5e, + 0x2c, 0xdc, 0x28, 0x56, 0x17, 0x04, 0x0c, 0x92, 0x4a, 0x0a}, + chaincfg.TestNet3Params.ScriptHashAddrID), + f: func() (btcutil.Address, error) { + hash := []byte{ + 0xc5, 0x79, 0x34, 0x2c, 0x2c, 0x4c, 0x92, 0x20, 0x20, 0x5e, + 0x2c, 0xdc, 0x28, 0x56, 0x17, 0x04, 0x0c, 0x92, 0x4a, 0x0a} + return btcutil.NewAddressScriptHashFromHash(hash, &chaincfg.TestNet3Params) + }, + net: &chaincfg.TestNet3Params, + }, + + // Negative P2SH tests. + { + name: "p2sh wrong hash length", + addr: "", + valid: false, + f: func() (btcutil.Address, error) { + hash := []byte{ + 0x00, 0xf8, 0x15, 0xb0, 0x36, 0xd9, 0xbb, 0xbc, 0xe5, 0xe9, + 0xf2, 0xa0, 0x0a, 0xbd, 0x1b, 0xf3, 0xdc, 0x91, 0xe9, 0x55, + 0x10} + return btcutil.NewAddressScriptHashFromHash(hash, &chaincfg.MainNetParams) + }, + net: &chaincfg.MainNetParams, + }, + + // Positive P2PK tests. + { + name: "mainnet p2pk compressed (0x02)", + addr: "02192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4", + encoded: "13CG6SJ3yHUXo4Cr2RY4THLLJrNFuG3gUg", + valid: true, + result: btcutil.TstAddressPubKey( + []byte{ + 0x02, 0x19, 0x2d, 0x74, 0xd0, 0xcb, 0x94, 0x34, 0x4c, 0x95, + 0x69, 0xc2, 0xe7, 0x79, 0x01, 0x57, 0x3d, 0x8d, 0x79, 0x03, + 0xc3, 0xeb, 0xec, 0x3a, 0x95, 0x77, 0x24, 0x89, 0x5d, 0xca, + 0x52, 0xc6, 0xb4}, + btcutil.PKFCompressed, chaincfg.MainNetParams.PubKeyHashAddrID), + f: func() (btcutil.Address, error) { + serializedPubKey := []byte{ + 0x02, 0x19, 0x2d, 0x74, 0xd0, 0xcb, 0x94, 0x34, 0x4c, 0x95, + 0x69, 0xc2, 0xe7, 0x79, 0x01, 0x57, 0x3d, 0x8d, 0x79, 0x03, + 0xc3, 0xeb, 0xec, 0x3a, 0x95, 0x77, 0x24, 0x89, 0x5d, 0xca, + 0x52, 0xc6, 0xb4} + return btcutil.NewAddressPubKey(serializedPubKey, &chaincfg.MainNetParams) + }, + net: &chaincfg.MainNetParams, + }, + { + name: "mainnet p2pk compressed (0x03)", + addr: "03b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e65", + encoded: "15sHANNUBSh6nDp8XkDPmQcW6n3EFwmvE6", + valid: true, + result: btcutil.TstAddressPubKey( + []byte{ + 0x03, 0xb0, 0xbd, 0x63, 0x42, 0x34, 0xab, 0xbb, 0x1b, 0xa1, + 0xe9, 0x86, 0xe8, 0x84, 0x18, 0x5c, 0x61, 0xcf, 0x43, 0xe0, + 0x01, 0xf9, 0x13, 0x7f, 0x23, 0xc2, 0xc4, 0x09, 0x27, 0x3e, + 0xb1, 0x6e, 0x65}, + btcutil.PKFCompressed, chaincfg.MainNetParams.PubKeyHashAddrID), + f: func() (btcutil.Address, error) { + serializedPubKey := []byte{ + 0x03, 0xb0, 0xbd, 0x63, 0x42, 0x34, 0xab, 0xbb, 0x1b, 0xa1, + 0xe9, 0x86, 0xe8, 0x84, 0x18, 0x5c, 0x61, 0xcf, 0x43, 0xe0, + 0x01, 0xf9, 0x13, 0x7f, 0x23, 0xc2, 0xc4, 0x09, 0x27, 0x3e, + 0xb1, 0x6e, 0x65} + return btcutil.NewAddressPubKey(serializedPubKey, &chaincfg.MainNetParams) + }, + net: &chaincfg.MainNetParams, + }, + { + name: "mainnet p2pk uncompressed (0x04)", + addr: "0411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2" + + "e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3", + encoded: "12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S", + valid: true, + result: btcutil.TstAddressPubKey( + []byte{ + 0x04, 0x11, 0xdb, 0x93, 0xe1, 0xdc, 0xdb, 0x8a, 0x01, 0x6b, + 0x49, 0x84, 0x0f, 0x8c, 0x53, 0xbc, 0x1e, 0xb6, 0x8a, 0x38, + 0x2e, 0x97, 0xb1, 0x48, 0x2e, 0xca, 0xd7, 0xb1, 0x48, 0xa6, + 0x90, 0x9a, 0x5c, 0xb2, 0xe0, 0xea, 0xdd, 0xfb, 0x84, 0xcc, + 0xf9, 0x74, 0x44, 0x64, 0xf8, 0x2e, 0x16, 0x0b, 0xfa, 0x9b, + 0x8b, 0x64, 0xf9, 0xd4, 0xc0, 0x3f, 0x99, 0x9b, 0x86, 0x43, + 0xf6, 0x56, 0xb4, 0x12, 0xa3}, + btcutil.PKFUncompressed, chaincfg.MainNetParams.PubKeyHashAddrID), + f: func() (btcutil.Address, error) { + serializedPubKey := []byte{ + 0x04, 0x11, 0xdb, 0x93, 0xe1, 0xdc, 0xdb, 0x8a, 0x01, 0x6b, + 0x49, 0x84, 0x0f, 0x8c, 0x53, 0xbc, 0x1e, 0xb6, 0x8a, 0x38, + 0x2e, 0x97, 0xb1, 0x48, 0x2e, 0xca, 0xd7, 0xb1, 0x48, 0xa6, + 0x90, 0x9a, 0x5c, 0xb2, 0xe0, 0xea, 0xdd, 0xfb, 0x84, 0xcc, + 0xf9, 0x74, 0x44, 0x64, 0xf8, 0x2e, 0x16, 0x0b, 0xfa, 0x9b, + 0x8b, 0x64, 0xf9, 0xd4, 0xc0, 0x3f, 0x99, 0x9b, 0x86, 0x43, + 0xf6, 0x56, 0xb4, 0x12, 0xa3} + return btcutil.NewAddressPubKey(serializedPubKey, &chaincfg.MainNetParams) + }, + net: &chaincfg.MainNetParams, + }, + { + name: "mainnet p2pk hybrid (0x06)", + addr: "06192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4" + + "0d45264838c0bd96852662ce6a847b197376830160c6d2eb5e6a4c44d33f453e", + encoded: "1Ja5rs7XBZnK88EuLVcFqYGMEbBitzchmX", + valid: true, + result: btcutil.TstAddressPubKey( + []byte{ + 0x06, 0x19, 0x2d, 0x74, 0xd0, 0xcb, 0x94, 0x34, 0x4c, 0x95, + 0x69, 0xc2, 0xe7, 0x79, 0x01, 0x57, 0x3d, 0x8d, 0x79, 0x03, + 0xc3, 0xeb, 0xec, 0x3a, 0x95, 0x77, 0x24, 0x89, 0x5d, 0xca, + 0x52, 0xc6, 0xb4, 0x0d, 0x45, 0x26, 0x48, 0x38, 0xc0, 0xbd, + 0x96, 0x85, 0x26, 0x62, 0xce, 0x6a, 0x84, 0x7b, 0x19, 0x73, + 0x76, 0x83, 0x01, 0x60, 0xc6, 0xd2, 0xeb, 0x5e, 0x6a, 0x4c, + 0x44, 0xd3, 0x3f, 0x45, 0x3e}, + btcutil.PKFHybrid, chaincfg.MainNetParams.PubKeyHashAddrID), + f: func() (btcutil.Address, error) { + serializedPubKey := []byte{ + 0x06, 0x19, 0x2d, 0x74, 0xd0, 0xcb, 0x94, 0x34, 0x4c, 0x95, + 0x69, 0xc2, 0xe7, 0x79, 0x01, 0x57, 0x3d, 0x8d, 0x79, 0x03, + 0xc3, 0xeb, 0xec, 0x3a, 0x95, 0x77, 0x24, 0x89, 0x5d, 0xca, + 0x52, 0xc6, 0xb4, 0x0d, 0x45, 0x26, 0x48, 0x38, 0xc0, 0xbd, + 0x96, 0x85, 0x26, 0x62, 0xce, 0x6a, 0x84, 0x7b, 0x19, 0x73, + 0x76, 0x83, 0x01, 0x60, 0xc6, 0xd2, 0xeb, 0x5e, 0x6a, 0x4c, + 0x44, 0xd3, 0x3f, 0x45, 0x3e} + return btcutil.NewAddressPubKey(serializedPubKey, &chaincfg.MainNetParams) + }, + net: &chaincfg.MainNetParams, + }, + { + name: "mainnet p2pk hybrid (0x07)", + addr: "07b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e65" + + "37a576782eba668a7ef8bd3b3cfb1edb7117ab65129b8a2e681f3c1e0908ef7b", + encoded: "1ExqMmf6yMxcBMzHjbj41wbqYuqoX6uBLG", + valid: true, + result: btcutil.TstAddressPubKey( + []byte{ + 0x07, 0xb0, 0xbd, 0x63, 0x42, 0x34, 0xab, 0xbb, 0x1b, 0xa1, + 0xe9, 0x86, 0xe8, 0x84, 0x18, 0x5c, 0x61, 0xcf, 0x43, 0xe0, + 0x01, 0xf9, 0x13, 0x7f, 0x23, 0xc2, 0xc4, 0x09, 0x27, 0x3e, + 0xb1, 0x6e, 0x65, 0x37, 0xa5, 0x76, 0x78, 0x2e, 0xba, 0x66, + 0x8a, 0x7e, 0xf8, 0xbd, 0x3b, 0x3c, 0xfb, 0x1e, 0xdb, 0x71, + 0x17, 0xab, 0x65, 0x12, 0x9b, 0x8a, 0x2e, 0x68, 0x1f, 0x3c, + 0x1e, 0x09, 0x08, 0xef, 0x7b}, + btcutil.PKFHybrid, chaincfg.MainNetParams.PubKeyHashAddrID), + f: func() (btcutil.Address, error) { + serializedPubKey := []byte{ + 0x07, 0xb0, 0xbd, 0x63, 0x42, 0x34, 0xab, 0xbb, 0x1b, 0xa1, + 0xe9, 0x86, 0xe8, 0x84, 0x18, 0x5c, 0x61, 0xcf, 0x43, 0xe0, + 0x01, 0xf9, 0x13, 0x7f, 0x23, 0xc2, 0xc4, 0x09, 0x27, 0x3e, + 0xb1, 0x6e, 0x65, 0x37, 0xa5, 0x76, 0x78, 0x2e, 0xba, 0x66, + 0x8a, 0x7e, 0xf8, 0xbd, 0x3b, 0x3c, 0xfb, 0x1e, 0xdb, 0x71, + 0x17, 0xab, 0x65, 0x12, 0x9b, 0x8a, 0x2e, 0x68, 0x1f, 0x3c, + 0x1e, 0x09, 0x08, 0xef, 0x7b} + return btcutil.NewAddressPubKey(serializedPubKey, &chaincfg.MainNetParams) + }, + net: &chaincfg.MainNetParams, + }, + { + name: "testnet p2pk compressed (0x02)", + addr: "02192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b4", + encoded: "mhiDPVP2nJunaAgTjzWSHCYfAqxxrxzjmo", + valid: true, + result: btcutil.TstAddressPubKey( + []byte{ + 0x02, 0x19, 0x2d, 0x74, 0xd0, 0xcb, 0x94, 0x34, 0x4c, 0x95, + 0x69, 0xc2, 0xe7, 0x79, 0x01, 0x57, 0x3d, 0x8d, 0x79, 0x03, + 0xc3, 0xeb, 0xec, 0x3a, 0x95, 0x77, 0x24, 0x89, 0x5d, 0xca, + 0x52, 0xc6, 0xb4}, + btcutil.PKFCompressed, chaincfg.TestNet3Params.PubKeyHashAddrID), + f: func() (btcutil.Address, error) { + serializedPubKey := []byte{ + 0x02, 0x19, 0x2d, 0x74, 0xd0, 0xcb, 0x94, 0x34, 0x4c, 0x95, + 0x69, 0xc2, 0xe7, 0x79, 0x01, 0x57, 0x3d, 0x8d, 0x79, 0x03, + 0xc3, 0xeb, 0xec, 0x3a, 0x95, 0x77, 0x24, 0x89, 0x5d, 0xca, + 0x52, 0xc6, 0xb4} + return btcutil.NewAddressPubKey(serializedPubKey, &chaincfg.TestNet3Params) + }, + net: &chaincfg.TestNet3Params, + }, + { + name: "testnet p2pk compressed (0x03)", + addr: "03b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e65", + encoded: "mkPETRTSzU8MZLHkFKBmbKppxmdw9qT42t", + valid: true, + result: btcutil.TstAddressPubKey( + []byte{ + 0x03, 0xb0, 0xbd, 0x63, 0x42, 0x34, 0xab, 0xbb, 0x1b, 0xa1, + 0xe9, 0x86, 0xe8, 0x84, 0x18, 0x5c, 0x61, 0xcf, 0x43, 0xe0, + 0x01, 0xf9, 0x13, 0x7f, 0x23, 0xc2, 0xc4, 0x09, 0x27, 0x3e, + 0xb1, 0x6e, 0x65}, + btcutil.PKFCompressed, chaincfg.TestNet3Params.PubKeyHashAddrID), + f: func() (btcutil.Address, error) { + serializedPubKey := []byte{ + 0x03, 0xb0, 0xbd, 0x63, 0x42, 0x34, 0xab, 0xbb, 0x1b, 0xa1, + 0xe9, 0x86, 0xe8, 0x84, 0x18, 0x5c, 0x61, 0xcf, 0x43, 0xe0, + 0x01, 0xf9, 0x13, 0x7f, 0x23, 0xc2, 0xc4, 0x09, 0x27, 0x3e, + 0xb1, 0x6e, 0x65} + return btcutil.NewAddressPubKey(serializedPubKey, &chaincfg.TestNet3Params) + }, + net: &chaincfg.TestNet3Params, + }, + { + name: "testnet p2pk uncompressed (0x04)", + addr: "0411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5" + + "cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3", + encoded: "mh8YhPYEAYs3E7EVyKtB5xrcfMExkkdEMF", + valid: true, + result: btcutil.TstAddressPubKey( + []byte{ + 0x04, 0x11, 0xdb, 0x93, 0xe1, 0xdc, 0xdb, 0x8a, 0x01, 0x6b, + 0x49, 0x84, 0x0f, 0x8c, 0x53, 0xbc, 0x1e, 0xb6, 0x8a, 0x38, + 0x2e, 0x97, 0xb1, 0x48, 0x2e, 0xca, 0xd7, 0xb1, 0x48, 0xa6, + 0x90, 0x9a, 0x5c, 0xb2, 0xe0, 0xea, 0xdd, 0xfb, 0x84, 0xcc, + 0xf9, 0x74, 0x44, 0x64, 0xf8, 0x2e, 0x16, 0x0b, 0xfa, 0x9b, + 0x8b, 0x64, 0xf9, 0xd4, 0xc0, 0x3f, 0x99, 0x9b, 0x86, 0x43, + 0xf6, 0x56, 0xb4, 0x12, 0xa3}, + btcutil.PKFUncompressed, chaincfg.TestNet3Params.PubKeyHashAddrID), + f: func() (btcutil.Address, error) { + serializedPubKey := []byte{ + 0x04, 0x11, 0xdb, 0x93, 0xe1, 0xdc, 0xdb, 0x8a, 0x01, 0x6b, + 0x49, 0x84, 0x0f, 0x8c, 0x53, 0xbc, 0x1e, 0xb6, 0x8a, 0x38, + 0x2e, 0x97, 0xb1, 0x48, 0x2e, 0xca, 0xd7, 0xb1, 0x48, 0xa6, + 0x90, 0x9a, 0x5c, 0xb2, 0xe0, 0xea, 0xdd, 0xfb, 0x84, 0xcc, + 0xf9, 0x74, 0x44, 0x64, 0xf8, 0x2e, 0x16, 0x0b, 0xfa, 0x9b, + 0x8b, 0x64, 0xf9, 0xd4, 0xc0, 0x3f, 0x99, 0x9b, 0x86, 0x43, + 0xf6, 0x56, 0xb4, 0x12, 0xa3} + return btcutil.NewAddressPubKey(serializedPubKey, &chaincfg.TestNet3Params) + }, + net: &chaincfg.TestNet3Params, + }, + { + name: "testnet p2pk hybrid (0x06)", + addr: "06192d74d0cb94344c9569c2e77901573d8d7903c3ebec3a957724895dca52c6b" + + "40d45264838c0bd96852662ce6a847b197376830160c6d2eb5e6a4c44d33f453e", + encoded: "my639vCVzbDZuEiX44adfTUg6anRomZLEP", + valid: true, + result: btcutil.TstAddressPubKey( + []byte{ + 0x06, 0x19, 0x2d, 0x74, 0xd0, 0xcb, 0x94, 0x34, 0x4c, 0x95, + 0x69, 0xc2, 0xe7, 0x79, 0x01, 0x57, 0x3d, 0x8d, 0x79, 0x03, + 0xc3, 0xeb, 0xec, 0x3a, 0x95, 0x77, 0x24, 0x89, 0x5d, 0xca, + 0x52, 0xc6, 0xb4, 0x0d, 0x45, 0x26, 0x48, 0x38, 0xc0, 0xbd, + 0x96, 0x85, 0x26, 0x62, 0xce, 0x6a, 0x84, 0x7b, 0x19, 0x73, + 0x76, 0x83, 0x01, 0x60, 0xc6, 0xd2, 0xeb, 0x5e, 0x6a, 0x4c, + 0x44, 0xd3, 0x3f, 0x45, 0x3e}, + btcutil.PKFHybrid, chaincfg.TestNet3Params.PubKeyHashAddrID), + f: func() (btcutil.Address, error) { + serializedPubKey := []byte{ + 0x06, 0x19, 0x2d, 0x74, 0xd0, 0xcb, 0x94, 0x34, 0x4c, 0x95, + 0x69, 0xc2, 0xe7, 0x79, 0x01, 0x57, 0x3d, 0x8d, 0x79, 0x03, + 0xc3, 0xeb, 0xec, 0x3a, 0x95, 0x77, 0x24, 0x89, 0x5d, 0xca, + 0x52, 0xc6, 0xb4, 0x0d, 0x45, 0x26, 0x48, 0x38, 0xc0, 0xbd, + 0x96, 0x85, 0x26, 0x62, 0xce, 0x6a, 0x84, 0x7b, 0x19, 0x73, + 0x76, 0x83, 0x01, 0x60, 0xc6, 0xd2, 0xeb, 0x5e, 0x6a, 0x4c, + 0x44, 0xd3, 0x3f, 0x45, 0x3e} + return btcutil.NewAddressPubKey(serializedPubKey, &chaincfg.TestNet3Params) + }, + net: &chaincfg.TestNet3Params, + }, + { + name: "testnet p2pk hybrid (0x07)", + addr: "07b0bd634234abbb1ba1e986e884185c61cf43e001f9137f23c2c409273eb16e6" + + "537a576782eba668a7ef8bd3b3cfb1edb7117ab65129b8a2e681f3c1e0908ef7b", + encoded: "muUnepk5nPPrxUTuTAhRqrpAQuSWS5fVii", + valid: true, + result: btcutil.TstAddressPubKey( + []byte{ + 0x07, 0xb0, 0xbd, 0x63, 0x42, 0x34, 0xab, 0xbb, 0x1b, 0xa1, + 0xe9, 0x86, 0xe8, 0x84, 0x18, 0x5c, 0x61, 0xcf, 0x43, 0xe0, + 0x01, 0xf9, 0x13, 0x7f, 0x23, 0xc2, 0xc4, 0x09, 0x27, 0x3e, + 0xb1, 0x6e, 0x65, 0x37, 0xa5, 0x76, 0x78, 0x2e, 0xba, 0x66, + 0x8a, 0x7e, 0xf8, 0xbd, 0x3b, 0x3c, 0xfb, 0x1e, 0xdb, 0x71, + 0x17, 0xab, 0x65, 0x12, 0x9b, 0x8a, 0x2e, 0x68, 0x1f, 0x3c, + 0x1e, 0x09, 0x08, 0xef, 0x7b}, + btcutil.PKFHybrid, chaincfg.TestNet3Params.PubKeyHashAddrID), + f: func() (btcutil.Address, error) { + serializedPubKey := []byte{ + 0x07, 0xb0, 0xbd, 0x63, 0x42, 0x34, 0xab, 0xbb, 0x1b, 0xa1, + 0xe9, 0x86, 0xe8, 0x84, 0x18, 0x5c, 0x61, 0xcf, 0x43, 0xe0, + 0x01, 0xf9, 0x13, 0x7f, 0x23, 0xc2, 0xc4, 0x09, 0x27, 0x3e, + 0xb1, 0x6e, 0x65, 0x37, 0xa5, 0x76, 0x78, 0x2e, 0xba, 0x66, + 0x8a, 0x7e, 0xf8, 0xbd, 0x3b, 0x3c, 0xfb, 0x1e, 0xdb, 0x71, + 0x17, 0xab, 0x65, 0x12, 0x9b, 0x8a, 0x2e, 0x68, 0x1f, 0x3c, + 0x1e, 0x09, 0x08, 0xef, 0x7b} + return btcutil.NewAddressPubKey(serializedPubKey, &chaincfg.TestNet3Params) + }, + net: &chaincfg.TestNet3Params, + }, + // Segwit address tests. + { + name: "segwit mainnet p2wpkh v0", + addr: "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", + encoded: "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", + valid: true, + result: btcutil.TstAddressWitnessPubKeyHash( + 0, + [20]byte{ + 0x75, 0x1e, 0x76, 0xe8, 0x19, 0x91, 0x96, 0xd4, 0x54, 0x94, + 0x1c, 0x45, 0xd1, 0xb3, 0xa3, 0x23, 0xf1, 0x43, 0x3b, 0xd6}, + chaincfg.MainNetParams.Bech32HRPSegwit), + f: func() (btcutil.Address, error) { + pkHash := []byte{ + 0x75, 0x1e, 0x76, 0xe8, 0x19, 0x91, 0x96, 0xd4, 0x54, 0x94, + 0x1c, 0x45, 0xd1, 0xb3, 0xa3, 0x23, 0xf1, 0x43, 0x3b, 0xd6} + return btcutil.NewAddressWitnessPubKeyHash(pkHash, &chaincfg.MainNetParams) + }, + net: &chaincfg.MainNetParams, + }, + { + name: "segwit mainnet p2wsh v0", + addr: "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3", + encoded: "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3", + valid: true, + result: btcutil.TstAddressWitnessScriptHash( + 0, + [32]byte{ + 0x18, 0x63, 0x14, 0x3c, 0x14, 0xc5, 0x16, 0x68, + 0x04, 0xbd, 0x19, 0x20, 0x33, 0x56, 0xda, 0x13, + 0x6c, 0x98, 0x56, 0x78, 0xcd, 0x4d, 0x27, 0xa1, + 0xb8, 0xc6, 0x32, 0x96, 0x04, 0x90, 0x32, 0x62}, + chaincfg.MainNetParams.Bech32HRPSegwit), + f: func() (btcutil.Address, error) { + scriptHash := []byte{ + 0x18, 0x63, 0x14, 0x3c, 0x14, 0xc5, 0x16, 0x68, + 0x04, 0xbd, 0x19, 0x20, 0x33, 0x56, 0xda, 0x13, + 0x6c, 0x98, 0x56, 0x78, 0xcd, 0x4d, 0x27, 0xa1, + 0xb8, 0xc6, 0x32, 0x96, 0x04, 0x90, 0x32, 0x62} + return btcutil.NewAddressWitnessScriptHash(scriptHash, &chaincfg.MainNetParams) + }, + net: &chaincfg.MainNetParams, + }, + { + name: "segwit testnet p2wpkh v0", + addr: "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx", + encoded: "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx", + valid: true, + result: btcutil.TstAddressWitnessPubKeyHash( + 0, + [20]byte{ + 0x75, 0x1e, 0x76, 0xe8, 0x19, 0x91, 0x96, 0xd4, 0x54, 0x94, + 0x1c, 0x45, 0xd1, 0xb3, 0xa3, 0x23, 0xf1, 0x43, 0x3b, 0xd6}, + chaincfg.TestNet3Params.Bech32HRPSegwit), + f: func() (btcutil.Address, error) { + pkHash := []byte{ + 0x75, 0x1e, 0x76, 0xe8, 0x19, 0x91, 0x96, 0xd4, 0x54, 0x94, + 0x1c, 0x45, 0xd1, 0xb3, 0xa3, 0x23, 0xf1, 0x43, 0x3b, 0xd6} + return btcutil.NewAddressWitnessPubKeyHash(pkHash, &chaincfg.TestNet3Params) + }, + net: &chaincfg.TestNet3Params, + }, + { + name: "segwit testnet p2wsh v0", + addr: "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", + encoded: "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7", + valid: true, + result: btcutil.TstAddressWitnessScriptHash( + 0, + [32]byte{ + 0x18, 0x63, 0x14, 0x3c, 0x14, 0xc5, 0x16, 0x68, + 0x04, 0xbd, 0x19, 0x20, 0x33, 0x56, 0xda, 0x13, + 0x6c, 0x98, 0x56, 0x78, 0xcd, 0x4d, 0x27, 0xa1, + 0xb8, 0xc6, 0x32, 0x96, 0x04, 0x90, 0x32, 0x62}, + chaincfg.TestNet3Params.Bech32HRPSegwit), + f: func() (btcutil.Address, error) { + scriptHash := []byte{ + 0x18, 0x63, 0x14, 0x3c, 0x14, 0xc5, 0x16, 0x68, + 0x04, 0xbd, 0x19, 0x20, 0x33, 0x56, 0xda, 0x13, + 0x6c, 0x98, 0x56, 0x78, 0xcd, 0x4d, 0x27, 0xa1, + 0xb8, 0xc6, 0x32, 0x96, 0x04, 0x90, 0x32, 0x62} + return btcutil.NewAddressWitnessScriptHash(scriptHash, &chaincfg.TestNet3Params) + }, + net: &chaincfg.TestNet3Params, + }, + { + name: "segwit testnet p2wsh witness v0", + addr: "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", + encoded: "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy", + valid: true, + result: btcutil.TstAddressWitnessScriptHash( + 0, + [32]byte{ + 0x00, 0x00, 0x00, 0xc4, 0xa5, 0xca, 0xd4, 0x62, + 0x21, 0xb2, 0xa1, 0x87, 0x90, 0x5e, 0x52, 0x66, + 0x36, 0x2b, 0x99, 0xd5, 0xe9, 0x1c, 0x6c, 0xe2, + 0x4d, 0x16, 0x5d, 0xab, 0x93, 0xe8, 0x64, 0x33}, + chaincfg.TestNet3Params.Bech32HRPSegwit), + f: func() (btcutil.Address, error) { + scriptHash := []byte{ + 0x00, 0x00, 0x00, 0xc4, 0xa5, 0xca, 0xd4, 0x62, + 0x21, 0xb2, 0xa1, 0x87, 0x90, 0x5e, 0x52, 0x66, + 0x36, 0x2b, 0x99, 0xd5, 0xe9, 0x1c, 0x6c, 0xe2, + 0x4d, 0x16, 0x5d, 0xab, 0x93, 0xe8, 0x64, 0x33} + return btcutil.NewAddressWitnessScriptHash(scriptHash, &chaincfg.TestNet3Params) + }, + net: &chaincfg.TestNet3Params, + }, + { + name: "segwit litecoin mainnet p2wpkh v0", + addr: "LTC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KGMN4N9", + encoded: "ltc1qw508d6qejxtdg4y5r3zarvary0c5xw7kgmn4n9", + valid: true, + result: btcutil.TstAddressWitnessPubKeyHash( + 0, + [20]byte{ + 0x75, 0x1e, 0x76, 0xe8, 0x19, 0x91, 0x96, 0xd4, 0x54, 0x94, + 0x1c, 0x45, 0xd1, 0xb3, 0xa3, 0x23, 0xf1, 0x43, 0x3b, 0xd6}, + CustomParams.Bech32HRPSegwit, + ), + f: func() (btcutil.Address, error) { + pkHash := []byte{ + 0x75, 0x1e, 0x76, 0xe8, 0x19, 0x91, 0x96, 0xd4, 0x54, 0x94, + 0x1c, 0x45, 0xd1, 0xb3, 0xa3, 0x23, 0xf1, 0x43, 0x3b, 0xd6} + return btcutil.NewAddressWitnessPubKeyHash(pkHash, &customParams) + }, + net: &customParams, + }, + // Unsupported witness versions (version 0 only supported at this point) + { + name: "segwit mainnet witness v1", + addr: "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx", + valid: false, + net: &chaincfg.MainNetParams, + }, + { + name: "segwit mainnet witness v16", + addr: "BC1SW50QA3JX3S", + valid: false, + net: &chaincfg.MainNetParams, + }, + { + name: "segwit mainnet witness v2", + addr: "bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj", + valid: false, + net: &chaincfg.MainNetParams, + }, + // Invalid segwit addresses + { + name: "segwit invalid hrp", + addr: "tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty", + valid: false, + net: &chaincfg.TestNet3Params, + }, + { + name: "segwit invalid checksum", + addr: "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5", + valid: false, + net: &chaincfg.MainNetParams, + }, + { + name: "segwit invalid witness version", + addr: "BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2", + valid: false, + net: &chaincfg.MainNetParams, + }, + { + name: "segwit invalid program length", + addr: "bc1rw5uspcuh", + valid: false, + net: &chaincfg.MainNetParams, + }, + { + name: "segwit invalid program length", + addr: "bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90", + valid: false, + net: &chaincfg.MainNetParams, + }, + { + name: "segwit invalid program length for witness version 0 (per BIP141)", + addr: "BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P", + valid: false, + net: &chaincfg.MainNetParams, + }, + { + name: "segwit mixed case", + addr: "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7", + valid: false, + net: &chaincfg.TestNet3Params, + }, + { + name: "segwit zero padding of more than 4 bits", + addr: "tb1pw508d6qejxtdg4y5r3zarqfsj6c3", + valid: false, + net: &chaincfg.TestNet3Params, + }, + { + name: "segwit non-zero padding in 8-to-5 conversion", + addr: "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv", + valid: false, + net: &chaincfg.TestNet3Params, + }, + } + + if err := chaincfg.Register(&customParams); err != nil { + panic(err) + } + + for _, test := range tests { + // Decode addr and compare error against valid. + decoded, err := btcutil.DecodeAddress(test.addr, test.net) + if (err == nil) != test.valid { + t.Errorf("%v: decoding test failed: %v", test.name, err) + return + } + + if err == nil { + // Ensure the stringer returns the same address as the + // original. + if decodedStringer, ok := decoded.(fmt.Stringer); ok { + addr := test.addr + + // For Segwit addresses the string representation + // will always be lower case, so in that case we + // convert the original to lower case first. + if strings.Contains(test.name, "segwit") { + addr = strings.ToLower(addr) + } + + if addr != decodedStringer.String() { + t.Errorf("%v: String on decoded value does not match expected value: %v != %v", + test.name, test.addr, decodedStringer.String()) + return + } + } + + // Encode again and compare against the original. + encoded := decoded.EncodeAddress() + if test.encoded != encoded { + t.Errorf("%v: decoding and encoding produced different addressess: %v != %v", + test.name, test.encoded, encoded) + return + } + + // Perform type-specific calculations. + var saddr []byte + switch d := decoded.(type) { + case *btcutil.AddressPubKeyHash: + saddr = btcutil.TstAddressSAddr(encoded) + + case *btcutil.AddressScriptHash: + saddr = btcutil.TstAddressSAddr(encoded) + + case *btcutil.AddressPubKey: + // Ignore the error here since the script + // address is checked below. + saddr, _ = hex.DecodeString(d.String()) + case *btcutil.AddressWitnessPubKeyHash: + saddr = btcutil.TstAddressSegwitSAddr(encoded) + case *btcutil.AddressWitnessScriptHash: + saddr = btcutil.TstAddressSegwitSAddr(encoded) + } + + // Check script address, as well as the Hash160 method for P2PKH and + // P2SH addresses. + if !bytes.Equal(saddr, decoded.ScriptAddress()) { + t.Errorf("%v: script addresses do not match:\n%x != \n%x", + test.name, saddr, decoded.ScriptAddress()) + return + } + switch a := decoded.(type) { + case *btcutil.AddressPubKeyHash: + if h := a.Hash160()[:]; !bytes.Equal(saddr, h) { + t.Errorf("%v: hashes do not match:\n%x != \n%x", + test.name, saddr, h) + return + } + + case *btcutil.AddressScriptHash: + if h := a.Hash160()[:]; !bytes.Equal(saddr, h) { + t.Errorf("%v: hashes do not match:\n%x != \n%x", + test.name, saddr, h) + return + } + + case *btcutil.AddressWitnessPubKeyHash: + if hrp := a.Hrp(); test.net.Bech32HRPSegwit != hrp { + t.Errorf("%v: hrps do not match:\n%x != \n%x", + test.name, test.net.Bech32HRPSegwit, hrp) + return + } + + expVer := test.result.(*btcutil.AddressWitnessPubKeyHash).WitnessVersion() + if v := a.WitnessVersion(); v != expVer { + t.Errorf("%v: witness versions do not match:\n%x != \n%x", + test.name, expVer, v) + return + } + + if p := a.WitnessProgram(); !bytes.Equal(saddr, p) { + t.Errorf("%v: witness programs do not match:\n%x != \n%x", + test.name, saddr, p) + return + } + + case *btcutil.AddressWitnessScriptHash: + if hrp := a.Hrp(); test.net.Bech32HRPSegwit != hrp { + t.Errorf("%v: hrps do not match:\n%x != \n%x", + test.name, test.net.Bech32HRPSegwit, hrp) + return + } + + expVer := test.result.(*btcutil.AddressWitnessScriptHash).WitnessVersion() + if v := a.WitnessVersion(); v != expVer { + t.Errorf("%v: witness versions do not match:\n%x != \n%x", + test.name, expVer, v) + return + } + + if p := a.WitnessProgram(); !bytes.Equal(saddr, p) { + t.Errorf("%v: witness programs do not match:\n%x != \n%x", + test.name, saddr, p) + return + } + } + + // Ensure the address is for the expected network. + if !decoded.IsForNet(test.net) { + t.Errorf("%v: calculated network does not match expected", + test.name) + return + } + } + + if !test.valid { + // If address is invalid, but a creation function exists, + // verify that it returns a nil addr and non-nil error. + if test.f != nil { + _, err := test.f() + if err == nil { + t.Errorf("%v: address is invalid but creating new address succeeded", + test.name) + return + } + } + continue + } + + // Valid test, compare address created with f against expected result. + addr, err := test.f() + if err != nil { + t.Errorf("%v: address is valid but creating new address failed with error %v", + test.name, err) + return + } + + if !reflect.DeepEqual(addr, test.result) { + t.Errorf("%v: created address does not match expected result", + test.name) + return + } + } +} diff --git a/tokens/zol/aggregate.go b/tokens/zol/aggregate.go new file mode 100644 index 00000000..cbd3d4d0 --- /dev/null +++ b/tokens/zol/aggregate.go @@ -0,0 +1,99 @@ +package zol + +import ( + "errors" + "time" + + "github.com/anyswap/CrossChain-Bridge/log" + "github.com/anyswap/CrossChain-Bridge/tokens" + "github.com/anyswap/CrossChain-Bridge/tokens/btc/electrs" +) + +const ( + redeemAggregateP2SHInputSize = 198 +) + +// ShouldAggregate should aggregate +func (b *Bridge) ShouldAggregate(aggUtxoCount int, aggSumVal uint64) bool { + if aggUtxoCount >= cfgUtxoAggregateMinCount { + return true + } + if aggSumVal >= cfgUtxoAggregateMinValue { + return true + } + return false +} + +// AggregateUtxos aggregate uxtos +func (b *Bridge) AggregateUtxos(addrs []string, utxos []*electrs.ElectUtxo) (string, error) { + relayFee, err := b.getRelayFeePerKb() + if err != nil { + return "", err + } + + authoredTx, err := b.BuildAggregateTransaction(relayFee, addrs, utxos) + if err != nil { + return "", err + } + + args := &tokens.BuildTxArgs{ + SwapInfo: tokens.SwapInfo{ + PairID: PairID, + Identifier: tokens.AggregateIdentifier, + }, + Extra: &tokens.AllExtras{ + BtcExtra: &tokens.BtcExtraArgs{}, + }, + } + + extra := args.Extra.BtcExtra + extra.RelayFeePerKb = &relayFee + extra.PreviousOutPoints = make([]*tokens.BtcOutPoint, len(authoredTx.Tx.TxIn)) + for i, txin := range authoredTx.Tx.TxIn { + point := txin.PreviousOutPoint + extra.PreviousOutPoints[i] = &tokens.BtcOutPoint{ + Hash: point.Hash.String(), + Index: point.Index, + } + } + + var signedTx interface{} + var txHash string + tokenCfg := b.GetTokenConfig(PairID) + if tokenCfg.GetDcrmAddressPrivateKey() != nil { + signedTx, txHash, err = b.SignTransaction(authoredTx, PairID) + } else { + maxRetryDcrmSignCount := 5 + for i := 0; i < maxRetryDcrmSignCount; i++ { + signedTx, txHash, err = b.DcrmSignTransaction(authoredTx, args.GetExtraArgs()) + if err == nil { + break + } + log.Warn("retry dcrm sign for aggregate", "count", i+1, "err", err) + time.Sleep(time.Second) + } + } + if err != nil { + return "", err + } + _, err = b.SendTransaction(signedTx) + if err != nil { + return "", err + } + return txHash, nil +} + +// VerifyAggregateMsgHash verify aggregate msgHash +func (b *Bridge) VerifyAggregateMsgHash(msgHash []string, args *tokens.BuildTxArgs) error { + if args == nil || args.Extra == nil || args.Extra.BtcExtra == nil || len(args.Extra.BtcExtra.PreviousOutPoints) == 0 { + return errors.New("empty btc extra") + } + if args.Extra.BtcExtra.RelayFeePerKb == nil { + return errors.New("empty relay fee") + } + rawTx, err := b.rebuildAggregateTransaction(args.Extra.BtcExtra) + if err != nil { + return err + } + return b.VerifyMsgHash(rawTx, msgHash) +} diff --git a/tokens/zol/bridge.go b/tokens/zol/bridge.go new file mode 100644 index 00000000..09d0d0e9 --- /dev/null +++ b/tokens/zol/bridge.go @@ -0,0 +1,94 @@ +package zol + +import ( + "fmt" + "strings" + "time" + + "github.com/anyswap/CrossChain-Bridge/log" + "github.com/anyswap/CrossChain-Bridge/tokens" +) + +const ( + netMainnet = "mainnet" + netTestnet3 = "testnet3" + netCustom = "custom" +) + +// PairID unique zol pair ID +var PairID = "zol" + +// Bridge btc bridge +type Bridge struct { + *tokens.CrossChainBridgeBase + Inherit Inheritable +} + +// NewCrossChainBridge new btc bridge +func NewCrossChainBridge(isSrc bool) *Bridge { + if !isSrc { + log.Fatalf("btc::NewCrossChainBridge error %v", tokens.ErrBridgeDestinationNotSupported) + } + instance := &Bridge{CrossChainBridgeBase: tokens.NewCrossChainBridgeBase(isSrc)} + BridgeInstance = instance + instance.SetInherit(instance) + return instance +} + +// SetInherit set inherit +func (b *Bridge) SetInherit(inherit Inheritable) { + b.Inherit = inherit +} + +// SetChainAndGateway set chain and gateway config +func (b *Bridge) SetChainAndGateway(chainCfg *tokens.ChainConfig, gatewayCfg *tokens.GatewayConfig) { + b.CrossChainBridgeBase.SetChainAndGateway(chainCfg, gatewayCfg) + b.VerifyChainConfig() + b.InitLatestBlockNumber() +} + +// VerifyChainConfig verify chain config +func (b *Bridge) VerifyChainConfig() { + chainCfg := b.ChainConfig + networkID := strings.ToLower(chainCfg.NetID) + switch networkID { + case netMainnet, netTestnet3: + case netCustom: + return + default: + log.Fatal("unsupported bitcoin network", "netID", chainCfg.NetID) + } +} + +// VerifyTokenConfig verify token config +func (b *Bridge) VerifyTokenConfig(tokenCfg *tokens.TokenConfig) error { + if !b.IsP2pkhAddress(tokenCfg.DcrmAddress) { + return fmt.Errorf("invalid dcrm address (not p2pkh): %v", tokenCfg.DcrmAddress) + } + if !b.IsValidAddress(tokenCfg.DepositAddress) { + return fmt.Errorf("invalid deposit address: %v", tokenCfg.DepositAddress) + } + if strings.EqualFold(tokenCfg.Symbol, "ZEROLIMIT") && *tokenCfg.Decimals != 8 { + return fmt.Errorf("invalid decimals for BTC: want 8 but have %v", *tokenCfg.Decimals) + } + return nil +} + +// InitLatestBlockNumber init latest block number +func (b *Bridge) InitLatestBlockNumber() { + chainCfg := b.ChainConfig + gatewayCfg := b.GatewayConfig + var latest uint64 + var err error + for { + latest, err = b.GetLatestBlockNumber() + if err == nil { + tokens.SetLatestBlockHeight(latest, b.IsSrc) + log.Info("get latst block number succeed.", "number", latest, "BlockChain", chainCfg.BlockChain, "NetID", chainCfg.NetID) + break + } + log.Error("get latst block number failed.", "BlockChain", chainCfg.BlockChain, "NetID", chainCfg.NetID, "err", err) + log.Println("retry query gateway", gatewayCfg.APIAddress) + time.Sleep(3 * time.Second) + } +} diff --git a/tokens/zol/btcdutils.go b/tokens/zol/btcdutils.go new file mode 100644 index 00000000..895a0dd6 --- /dev/null +++ b/tokens/zol/btcdutils.go @@ -0,0 +1,179 @@ +package zol + +import ( + "crypto/ecdsa" + "fmt" + "math/big" + "strings" + + "github.com/btcsuite/btcd/btcec" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" +) + +// Inheritable interface +type Inheritable interface { + GetChainParams() *chaincfg.Params +} + +type btcAmountType = btcutil.Amount +type wireTxInType = wire.TxIn +type wireTxOutType = wire.TxOut + +func disasmScriptToString(pkScript []byte) (string, error) { + return txscript.DisasmString(pkScript) +} + +func isValidValue(value btcAmountType) bool { + return value > 0 && value <= btcutil.MaxSatoshi +} + +func newAmount(value float64) (btcAmountType, error) { + return btcutil.NewAmount(value) +} + +// GetChainParams get chain config (net params) +func (b *Bridge) GetChainParams() *chaincfg.Params { + networkID := strings.ToLower(b.ChainConfig.NetID) + switch networkID { + case netMainnet: + params := &chaincfg.MainNetParams + params.PubKeyHashAddrID = 0x81 + return params + default: + params := &chaincfg.TestNet3Params + params.PubKeyHashAddrID = 0x81 + return params + } +} + +// ParsePkScript parse pkScript +func (b *Bridge) ParsePkScript(pkScript []byte) (txscript.PkScript, error) { + return txscript.ParsePkScript(pkScript) +} + +// GetPayToAddrScript get pay to address script +func (b *Bridge) GetPayToAddrScript(address string) ([]byte, error) { + toAddr, err := b.DecodeAddress(address) + if err != nil { + return nil, fmt.Errorf("decode btc address '%v' failed. %v", address, err) + } + return txscript.PayToAddrScript(toAddr) +} + +// GetP2shRedeemScript get p2sh redeem script +func (b *Bridge) GetP2shRedeemScript(memo, pubKeyHash []byte) (redeemScript []byte, err error) { + return txscript.NewScriptBuilder(). + AddData(memo).AddOp(txscript.OP_DROP). + AddOp(txscript.OP_DUP).AddOp(txscript.OP_HASH160).AddData(pubKeyHash). + AddOp(txscript.OP_EQUALVERIFY).AddOp(txscript.OP_CHECKSIG). + Script() +} + +// NullDataScript encap +func (b *Bridge) NullDataScript(memo string) ([]byte, error) { + return txscript.NullDataScript([]byte(memo)) +} + +// IsPayToScriptHash is p2sh +func (b *Bridge) IsPayToScriptHash(sigScript []byte) bool { + return txscript.IsPayToScriptHash(sigScript) +} + +// CalcSignatureHash calc sig hash +func (b *Bridge) CalcSignatureHash(sigScript []byte, tx *wire.MsgTx, i int) (sigHash []byte, err error) { + return txscript.CalcSignatureHash(sigScript, txscript.SigHashAll, tx, i) +} + +// SerializeSignature serialize signature +func (b *Bridge) SerializeSignature(r, s *big.Int) []byte { + sign := &btcec.Signature{R: r, S: s} + return append(sign.Serialize(), byte(txscript.SigHashAll)) +} + +// GetSigScript get script +func (b *Bridge) GetSigScript(sigScripts [][]byte, prevScript, signData, cPkData []byte, i int) (sigScript []byte, err error) { + scriptClass := txscript.GetScriptClass(prevScript) + switch scriptClass { + case txscript.PubKeyHashTy: + sigScript, err = txscript.NewScriptBuilder().AddData(signData).AddData(cPkData).Script() + case txscript.ScriptHashTy: + if sigScripts == nil { + err = fmt.Errorf("call MakeSignedTransaction spend p2sh without redeem scripts") + } else { + redeemScript := sigScripts[i] + err = b.VerifyRedeemScript(prevScript, redeemScript) + if err == nil { + sigScript, err = txscript.NewScriptBuilder().AddData(signData).AddData(cPkData).AddData(redeemScript).Script() + } + } + default: + err = fmt.Errorf("unsupport to spend '%v' output", scriptClass.String()) + } + return sigScript, err +} + +// SerializePublicKey serialize ecdsa public key +func (b *Bridge) SerializePublicKey(ecPub *ecdsa.PublicKey, compressed bool) []byte { + if compressed { + return (*btcec.PublicKey)(ecPub).SerializeCompressed() + } + return (*btcec.PublicKey)(ecPub).SerializeUncompressed() +} + +// ToCompressedPublicKey convert to compressed public key if not +func (b *Bridge) ToCompressedPublicKey(pkData []byte) ([]byte, error) { + pubKey, err := btcec.ParsePubKey(pkData, btcec.S256()) + if err != nil { + return nil, err + } + return pubKey.SerializeCompressed(), nil +} + +// GetPublicKeyFromECDSA get public key from ecdsa private key +func (b *Bridge) GetPublicKeyFromECDSA(privKey *ecdsa.PrivateKey, compressed bool) []byte { + if compressed { + return (*btcec.PublicKey)(&privKey.PublicKey).SerializeCompressed() + } + return (*btcec.PublicKey)(&privKey.PublicKey).SerializeUncompressed() +} + +// SignWithECDSA sign with ecdsa private key +func (b *Bridge) SignWithECDSA(privKey *ecdsa.PrivateKey, msgHash []byte) (rsv string, err error) { + signature, err := (*btcec.PrivateKey)(privKey).Sign(msgHash) + if err != nil { + return "", err + } + rr := fmt.Sprintf("%064X", signature.R) + ss := fmt.Sprintf("%064X", signature.S) + rsv = fmt.Sprintf("%s%s00", rr, ss) + return rsv, nil +} + +// NewTxIn new txin +func (b *Bridge) NewTxIn(txid string, vout uint32, pkScript []byte) (*wire.TxIn, error) { + txHash, err := chainhash.NewHashFromStr(txid) + if err != nil { + return nil, err + } + prevOutPoint := wire.NewOutPoint(txHash, vout) + return wire.NewTxIn(prevOutPoint, pkScript, nil), nil +} + +// NewTxOut new txout +func (b *Bridge) NewTxOut(amount int64, pkScript []byte) *wire.TxOut { + return wire.NewTxOut(amount, pkScript) +} + +// NewMsgTx new msg tx +func (b *Bridge) NewMsgTx(inputs []*wire.TxIn, outputs []*wire.TxOut, locktime uint32) *wire.MsgTx { + return &wire.MsgTx{ + Version: wire.TxVersion, + TxIn: inputs, + TxOut: outputs, + LockTime: locktime, + } +} diff --git a/tokens/zol/buildaggregatetx.go b/tokens/zol/buildaggregatetx.go new file mode 100644 index 00000000..650caef1 --- /dev/null +++ b/tokens/zol/buildaggregatetx.go @@ -0,0 +1,144 @@ +package zol + +import ( + "fmt" + "time" + + "github.com/anyswap/CrossChain-Bridge/log" + "github.com/anyswap/CrossChain-Bridge/tokens" + "github.com/anyswap/CrossChain-Bridge/tokens/btc/electrs" + "github.com/anyswap/CrossChain-Bridge/tokens/tools" + "github.com/btcsuite/btcwallet/wallet/txauthor" +) + +// BuildAggregateTransaction build aggregate tx (spend p2sh utxo) +func (b *Bridge) BuildAggregateTransaction(relayFeePerKb int64, addrs []string, utxos []*electrs.ElectUtxo) (rawTx *txauthor.AuthoredTx, err error) { + if len(addrs) != len(utxos) { + return nil, fmt.Errorf("call BuildAggregateTransaction: count of addrs (%v) is not equal to count of utxos (%v)", len(addrs), len(utxos)) + } + + txOuts, err := b.getTxOutputs("", nil, tokens.AggregateMemo) + if err != nil { + return nil, err + } + + inputSource := func(target btcAmountType) (total btcAmountType, inputs []*wireTxInType, inputValues []btcAmountType, scripts [][]byte, err error) { + return b.getUtxosFromElectUtxos(target, addrs, utxos) + } + + changeSource := func() ([]byte, error) { + return b.GetPayToAddrScript(cfgUtxoAggregateToAddress) + } + + return b.NewUnsignedTransaction(txOuts, btcAmountType(relayFeePerKb), inputSource, changeSource, true) +} + +func (b *Bridge) rebuildAggregateTransaction(extra *tokens.BtcExtraArgs) (rawTx *txauthor.AuthoredTx, err error) { + addrs, utxos, err := b.getUtxosFromOutPoints(extra.PreviousOutPoints) + if err != nil { + return nil, err + } + return b.BuildAggregateTransaction(*extra.RelayFeePerKb, addrs, utxos) +} + +func (b *Bridge) getUtxosFromElectUtxos(target btcAmountType, addrs []string, utxos []*electrs.ElectUtxo) (total btcAmountType, inputs []*wireTxInType, inputValues []btcAmountType, scripts [][]byte, err error) { + for i, utxo := range utxos { + value := btcAmountType(*utxo.Value) + if value == 0 { + continue + } + + address := addrs[i] + if b.IsP2shAddress(address) { + bindAddr := tools.GetP2shBindAddress(address) + if bindAddr == "" { + continue + } + p2shAddr, _, _ := b.GetP2shAddress(bindAddr) + if p2shAddr != address { + log.Warn("wrong registered p2sh address", "have", address, "bind", bindAddr, "want", p2shAddr) + continue + } + } + + pkScript, errt := b.GetPayToAddrScript(address) + if errt != nil { + continue + } + + txIn, errf := b.NewTxIn(*utxo.Txid, *utxo.Vout, pkScript) + if errf != nil { + continue + } + + total += value + inputs = append(inputs, txIn) + inputValues = append(inputValues, value) + scripts = append(scripts, pkScript) + } + + if total < target { + log.Warn("getUtxos total %v < target %v", total, target) + } + + return total, inputs, inputValues, scripts, nil +} + +func (b *Bridge) getUtxosFromOutPoints(prevOutPoints []*tokens.BtcOutPoint) (addrs []string, utxos []*electrs.ElectUtxo, err error) { + var ( + tx *electrs.ElectTx + outspend *electrs.ElectOutspend + ) + + for _, point := range prevOutPoints { + for i := 0; i < retryCount; i++ { + outspend, err = b.GetOutspend(point.Hash, point.Index) + if err == nil { + break + } + time.Sleep(retryInterval) + } + if err != nil { + return nil, nil, err + } + if *outspend.Spent { + if outspend.Status != nil && outspend.Status.BlockHeight != nil { + spentHeight := *outspend.Status.BlockHeight + err = fmt.Errorf("out point (%v, %v) is spent at %v", point.Hash, point.Index, spentHeight) + } else { + err = fmt.Errorf("out point (%v, %v) is spent at txpool", point.Hash, point.Index) + } + return nil, nil, err + } + for i := 0; i < retryCount; i++ { + tx, err = b.GetTransactionByHash(point.Hash) + if err == nil { + break + } + time.Sleep(retryInterval) + } + if err != nil { + return nil, nil, err + } + if point.Index >= uint32(len(tx.Vout)) { + err = fmt.Errorf("out point (%v, %v) index overflow", point.Hash, point.Index) + return nil, nil, err + } + output := tx.Vout[point.Index] + if *output.Value == 0 { + err = fmt.Errorf("out point (%v, %v) with zero value", point.Hash, point.Index) + return nil, nil, err + } + if output.ScriptpubkeyAddress == nil { + continue + } + + addrs = append(addrs, *output.ScriptpubkeyAddress) + utxos = append(utxos, &electrs.ElectUtxo{ + Txid: &point.Hash, + Vout: &point.Index, + Value: output.Value, + }) + } + return addrs, utxos, nil +} diff --git a/tokens/zol/buildtx.go b/tokens/zol/buildtx.go new file mode 100644 index 00000000..2929e4d4 --- /dev/null +++ b/tokens/zol/buildtx.go @@ -0,0 +1,470 @@ +package zol + +import ( + "errors" + "fmt" + "math/big" + "time" + + "github.com/anyswap/CrossChain-Bridge/log" + "github.com/anyswap/CrossChain-Bridge/params" + "github.com/anyswap/CrossChain-Bridge/tokens" + "github.com/anyswap/CrossChain-Bridge/tokens/btc/electrs" + "github.com/btcsuite/btcwallet/wallet/txauthor" + "github.com/btcsuite/btcwallet/wallet/txrules" + "github.com/btcsuite/btcwallet/wallet/txsizes" +) + +const ( + p2pkhType = "p2pkh" + p2shType = "p2sh" + opReturnType = "op_return" + + retryCount = 3 + retryInterval = 3 * time.Second +) + +func (b *Bridge) getRelayFeePerKb() (estimateFee int64, err error) { + for i := 0; i < retryCount; i++ { + estimateFee, err = b.EstimateFeePerKb(cfgEstimateFeeBlocks) + if err == nil { + break + } + time.Sleep(retryInterval) + } + if err != nil { + log.Warn("estimate smart fee failed", "err", err) + return 0, err + } + if cfgPlusFeePercentage > 0 { + estimateFee += estimateFee * int64(cfgPlusFeePercentage) / 100 + } + if estimateFee > cfgMaxRelayFeePerKb { + estimateFee = cfgMaxRelayFeePerKb + } else if estimateFee < cfgMinRelayFeePerKb { + estimateFee = cfgMinRelayFeePerKb + } + return estimateFee, nil +} + +// BuildRawTransaction build raw tx +func (b *Bridge) BuildRawTransaction(args *tokens.BuildTxArgs) (rawTx interface{}, err error) { + var ( + pairID = args.PairID + token = b.GetTokenConfig(pairID) + from = args.From + to = args.To + changeAddress = args.From + amount = args.Value + memo = args.Memo + relayFeePerKb btcAmountType + ) + + if token == nil { + return nil, fmt.Errorf("swap pair '%v' is not configed", pairID) + } + + switch args.SwapType { + case tokens.SwapinType: + return nil, tokens.ErrSwapTypeNotSupported + case tokens.SwapoutType: + from = token.DcrmAddress // from + to = args.Bind // to + changeAddress = token.DcrmAddress // change + amount = tokens.CalcSwappedValue(pairID, args.OriginValue, false) // amount + memo = tokens.UnlockMemoPrefix + args.SwapID + } + + if from == "" { + return nil, errors.New("no sender specified") + } + + var extra *tokens.BtcExtraArgs + if args.Extra == nil || args.Extra.BtcExtra == nil { + extra = &tokens.BtcExtraArgs{} + args.Extra = &tokens.AllExtras{BtcExtra: extra} + } else { + extra = args.Extra.BtcExtra + if extra.ChangeAddress != nil && args.SwapType == tokens.NoSwapType { + changeAddress = *extra.ChangeAddress + } + } + + if extra.RelayFeePerKb != nil { + relayFeePerKb = btcAmountType(*extra.RelayFeePerKb) + } else { + relayFee, errf := b.getRelayFeePerKb() + if errf != nil { + return nil, errf + } + extra.RelayFeePerKb = &relayFee + relayFeePerKb = btcAmountType(relayFee) + } + + txOuts, err := b.getTxOutputs(to, amount, memo) + if err != nil { + return nil, err + } + + inputSource := func(target btcAmountType) (total btcAmountType, inputs []*wireTxInType, inputValues []btcAmountType, scripts [][]byte, err error) { + if len(extra.PreviousOutPoints) != 0 { + return b.getUtxos(from, target, extra.PreviousOutPoints) + } + return b.selectUtxos(from, target) + } + + changeSource := func() ([]byte, error) { + return b.GetPayToAddrScript(changeAddress) + } + + authoredTx, err := b.NewUnsignedTransaction(txOuts, relayFeePerKb, inputSource, changeSource, false) + if err != nil { + return nil, err + } + + updateExtraInfo(extra, authoredTx.Tx.TxIn) + + if args.SwapType != tokens.NoSwapType { + args.Identifier = params.GetIdentifier() + } + + return authoredTx, nil +} + +func updateExtraInfo(extra *tokens.BtcExtraArgs, txins []*wireTxInType) { + if len(extra.PreviousOutPoints) > 0 { + return + } + extra.PreviousOutPoints = make([]*tokens.BtcOutPoint, len(txins)) + for i, txin := range txins { + point := txin.PreviousOutPoint + extra.PreviousOutPoints[i] = &tokens.BtcOutPoint{ + Hash: point.Hash.String(), + Index: point.Index, + } + } +} + +// BuildTransaction build tx +func (b *Bridge) BuildTransaction(from string, receivers []string, amounts []int64, memo string, relayFeePerKb int64) (rawTx interface{}, err error) { + if len(receivers) != len(amounts) { + return nil, fmt.Errorf("count of receivers and amounts are not equal") + } + + var txOuts []*wireTxOutType + + for i, receiver := range receivers { + err = b.addPayToAddrOutput(&txOuts, receiver, amounts[i]) + if err != nil { + return nil, err + } + } + + err = b.addMemoOutput(&txOuts, memo) + if err != nil { + return nil, err + } + + inputSource := func(target btcAmountType) (total btcAmountType, inputs []*wireTxInType, inputValues []btcAmountType, scripts [][]byte, err error) { + return b.selectUtxos(from, target) + } + + changeSource := func() ([]byte, error) { + return b.GetPayToAddrScript(from) + } + + return b.NewUnsignedTransaction(txOuts, btcAmountType(relayFeePerKb), inputSource, changeSource, false) +} + +func (b *Bridge) getTxOutputs(to string, amount *big.Int, memo string) (txOuts []*wireTxOutType, err error) { + if amount != nil { + err = b.addPayToAddrOutput(&txOuts, to, amount.Int64()) + if err != nil { + return nil, err + } + } + + if memo != "" { + err = b.addMemoOutput(&txOuts, memo) + if err != nil { + return nil, err + } + } + + return txOuts, err +} + +func (b *Bridge) addPayToAddrOutput(txOuts *[]*wireTxOutType, to string, amount int64) error { + if amount <= 0 { + return nil + } + pkscript, err := b.GetPayToAddrScript(to) + if err != nil { + return err + } + *txOuts = append(*txOuts, b.NewTxOut(amount, pkscript)) + return nil +} + +func (b *Bridge) addMemoOutput(txOuts *[]*wireTxOutType, memo string) error { + if memo == "" { + return nil + } + nullScript, err := b.NullDataScript(memo) + if err != nil { + return err + } + *txOuts = append(*txOuts, b.NewTxOut(0, nullScript)) + return nil +} + +func (b *Bridge) findUxtosWithRetry(from string) (utxos []*electrs.ElectUtxo, err error) { + for i := 0; i < retryCount; i++ { + utxos, err = b.FindUtxos(from) + if err == nil { + break + } + time.Sleep(retryInterval) + } + return utxos, err +} + +func (b *Bridge) getTransactionByHashWithRetry(txid string) (tx *electrs.ElectTx, err error) { + for i := 0; i < retryCount; i++ { + tx, err = b.GetTransactionByHash(txid) + if err == nil { + break + } + time.Sleep(retryInterval) + } + return tx, err +} + +func (b *Bridge) getOutspendWithRetry(point *tokens.BtcOutPoint) (outspend *electrs.ElectOutspend, err error) { + for i := 0; i < retryCount; i++ { + outspend, err = b.GetOutspend(point.Hash, point.Index) + if err == nil { + break + } + time.Sleep(retryInterval) + } + return outspend, err +} + +func (b *Bridge) selectUtxos(from string, target btcAmountType) (total btcAmountType, inputs []*wireTxInType, inputValues []btcAmountType, scripts [][]byte, err error) { + p2pkhScript, err := b.GetPayToAddrScript(from) + if err != nil { + return 0, nil, nil, nil, err + } + + utxos, err := b.findUxtosWithRetry(from) + if err != nil { + return 0, nil, nil, nil, err + } + + var ( + tx *electrs.ElectTx + success bool + ) + + for _, utxo := range utxos { + value := btcAmountType(*utxo.Value) + if !isValidValue(value) { + continue + } + tx, err = b.getTransactionByHashWithRetry(*utxo.Txid) + if err != nil { + continue + } + if *utxo.Vout >= uint32(len(tx.Vout)) { + continue + } + output := tx.Vout[*utxo.Vout] + if *output.ScriptpubkeyType != p2pkhType { + continue + } + if output.ScriptpubkeyAddress == nil || *output.ScriptpubkeyAddress != from { + continue + } + + txIn, errf := b.NewTxIn(*utxo.Txid, *utxo.Vout, p2pkhScript) + if errf != nil { + continue + } + + total += value + inputs = append(inputs, txIn) + inputValues = append(inputValues, value) + scripts = append(scripts, p2pkhScript) + + if total >= target { + success = true + break + } + } + + if !success { + err = fmt.Errorf("not enough balance, total %v < target %v", total, target) + return 0, nil, nil, nil, err + } + + return total, inputs, inputValues, scripts, nil +} + +func (b *Bridge) getUtxos(from string, target btcAmountType, prevOutPoints []*tokens.BtcOutPoint) (total btcAmountType, inputs []*wireTxInType, inputValues []btcAmountType, scripts [][]byte, err error) { + p2pkhScript, err := b.GetPayToAddrScript(from) + if err != nil { + return 0, nil, nil, nil, err + } + + for _, point := range prevOutPoints { + outspend, errf := b.getOutspendWithRetry(point) + if errf != nil { + return 0, nil, nil, nil, errf + } + if *outspend.Spent { + if outspend.Status != nil && outspend.Status.BlockHeight != nil { + spentHeight := *outspend.Status.BlockHeight + err = fmt.Errorf("out point (%v, %v) is spent at %v", point.Hash, point.Index, spentHeight) + } else { + err = fmt.Errorf("out point (%v, %v) is spent at txpool", point.Hash, point.Index) + } + return 0, nil, nil, nil, err + } + tx, errf := b.getTransactionByHashWithRetry(point.Hash) + if errf != nil { + return 0, nil, nil, nil, errf + } + if point.Index >= uint32(len(tx.Vout)) { + err = fmt.Errorf("out point (%v, %v) index overflow", point.Hash, point.Index) + return 0, nil, nil, nil, err + } + output := tx.Vout[point.Index] + if *output.ScriptpubkeyType != p2pkhType { + err = fmt.Errorf("out point (%v, %v) script pubkey type %v is not p2pkh", point.Hash, point.Index, *output.ScriptpubkeyType) + return 0, nil, nil, nil, err + } + if output.ScriptpubkeyAddress == nil || *output.ScriptpubkeyAddress != from { + err = fmt.Errorf("out point (%v, %v) script pubkey address %v is not %v", point.Hash, point.Index, *output.ScriptpubkeyAddress, from) + return 0, nil, nil, nil, err + } + value := btcAmountType(*output.Value) + if value == 0 { + err = fmt.Errorf("out point (%v, %v) with zero value", point.Hash, point.Index) + return 0, nil, nil, nil, err + } + + txIn, errf := b.NewTxIn(point.Hash, point.Index, p2pkhScript) + if errf != nil { + return 0, nil, nil, nil, errf + } + + total += value + inputs = append(inputs, txIn) + inputValues = append(inputValues, value) + scripts = append(scripts, p2pkhScript) + } + if total < target { + err = fmt.Errorf("not enough balance, total %v < target %v", total, target) + return 0, nil, nil, nil, err + } + return total, inputs, inputValues, scripts, nil +} + +type insufficientFundsError struct{} + +func (insufficientFundsError) InputSourceError() {} +func (insufficientFundsError) Error() string { + return "insufficient funds available to construct transaction" +} + +// NewUnsignedTransaction ref btcwallet +// ref. https://github.com/btcsuite/btcwallet/blob/b07494fc2d662fdda2b8a9db2a3eacde3e1ef347/wallet/txauthor/author.go +// we only modify it to support P2PKH change script (the origin only support P2WPKH change script) +// and update estimate size because we are not use P2WKH +func (b *Bridge) NewUnsignedTransaction(outputs []*wireTxOutType, relayFeePerKb btcAmountType, fetchInputs txauthor.InputSource, fetchChange txauthor.ChangeSource, isAggregate bool) (*txauthor.AuthoredTx, error) { + targetAmount := txauthor.SumOutputValues(outputs) + estimatedSize := txsizes.EstimateSerializeSize(1, outputs, true) + targetFee := txrules.FeeForSerializeSize(relayFeePerKb, estimatedSize) + + for { + inputAmount, inputs, inputValues, scripts, err := fetchInputs(targetAmount + targetFee) + if err != nil { + return nil, err + } + if inputAmount < targetAmount+targetFee { + return nil, insufficientFundsError{} + } + + maxSignedSize := b.estimateSize(scripts, outputs, true, isAggregate) + maxRequiredFee := txrules.FeeForSerializeSize(relayFeePerKb, maxSignedSize) + if maxRequiredFee < btcAmountType(cfgMinRelayFee) { + maxRequiredFee = btcAmountType(cfgMinRelayFee) + } + remainingAmount := inputAmount - targetAmount + if remainingAmount < maxRequiredFee { + if isAggregate { + return nil, insufficientFundsError{} + } + targetFee = maxRequiredFee + continue + } + + unsignedTransaction := b.NewMsgTx(inputs, outputs, 0) + + changeIndex := -1 + changeAmount := inputAmount - targetAmount - maxRequiredFee + if changeAmount != 0 { + changeScript, err := fetchChange() + if err != nil { + return nil, err + } + // commont this to support P2PKH change script + // if len(changeScript) > txsizes.P2WPKHPkScriptSize { + // return nil, errors.New("fee estimation requires change " + + // "scripts no larger than P2WPKH output scripts") + //} + threshold := txrules.GetDustThreshold(len(changeScript), txrules.DefaultRelayFeePerKb) + if changeAmount < threshold { + log.Debug("get rid of dust change", "amount", changeAmount, "threshold", threshold, "scriptsize", len(changeScript)) + } else { + change := b.NewTxOut(int64(changeAmount), changeScript) + l := len(outputs) + outputs = append(outputs[:l:l], change) + unsignedTransaction.TxOut = outputs + changeIndex = l + } + } + + return &txauthor.AuthoredTx{ + Tx: unsignedTransaction, + PrevScripts: scripts, + PrevInputValues: inputValues, + TotalInput: inputAmount, + ChangeIndex: changeIndex, + }, nil + } +} + +func (b *Bridge) estimateSize(scripts [][]byte, txOuts []*wireTxOutType, addChangeOutput, isAggregate bool) int { + if !isAggregate { + return txsizes.EstimateSerializeSize(len(scripts), txOuts, addChangeOutput) + } + + var p2sh, p2pkh int + for _, pkScript := range scripts { + switch { + case b.IsPayToScriptHash(pkScript): + p2sh++ + default: + p2pkh++ + } + } + + size := txsizes.EstimateSerializeSize(p2pkh, txOuts, addChangeOutput) + if p2sh > 0 { + size += p2sh * redeemAggregateP2SHInputSize + } + + return size +} diff --git a/tokens/zol/callapi.go b/tokens/zol/callapi.go new file mode 100644 index 00000000..8c04633d --- /dev/null +++ b/tokens/zol/callapi.go @@ -0,0 +1,106 @@ +package zol + +import ( + "fmt" + "math/big" + + "github.com/anyswap/CrossChain-Bridge/tokens/btc/electrs" +) + +// GetLatestBlockNumberOf impl +func (b *Bridge) GetLatestBlockNumberOf(apiAddress string) (uint64, error) { + return electrs.GetLatestBlockNumberOf(apiAddress) +} + +// GetLatestBlockNumber impl +func (b *Bridge) GetLatestBlockNumber() (uint64, error) { + return electrs.GetLatestBlockNumber(b) +} + +// GetTransactionByHash impl +func (b *Bridge) GetTransactionByHash(txHash string) (*electrs.ElectTx, error) { + return electrs.GetTransactionByHash(b, txHash) +} + +// GetElectTransactionStatus impl +func (b *Bridge) GetElectTransactionStatus(txHash string) (*electrs.ElectTxStatus, error) { + return electrs.GetElectTransactionStatus(b, txHash) +} + +// FindUtxos impl +func (b *Bridge) FindUtxos(addr string) ([]*electrs.ElectUtxo, error) { + return electrs.FindUtxos(b, addr) +} + +// GetPoolTxidList impl +func (b *Bridge) GetPoolTxidList() ([]string, error) { + return electrs.GetPoolTxidList(b) +} + +// GetPoolTransactions impl +func (b *Bridge) GetPoolTransactions(addr string) ([]*electrs.ElectTx, error) { + return electrs.GetPoolTransactions(b, addr) +} + +// GetTransactionHistory impl +func (b *Bridge) GetTransactionHistory(addr, lastSeenTxid string) ([]*electrs.ElectTx, error) { + return electrs.GetTransactionHistory(b, addr, lastSeenTxid) +} + +// GetOutspend impl +func (b *Bridge) GetOutspend(txHash string, vout uint32) (*electrs.ElectOutspend, error) { + return electrs.GetOutspend(b, txHash, vout) +} + +// PostTransaction impl +func (b *Bridge) PostTransaction(txHex string) (txHash string, err error) { + return electrs.PostTransaction(b, txHex) +} + +// GetBlockHash impl +func (b *Bridge) GetBlockHash(height uint64) (string, error) { + return electrs.GetBlockHash(b, height) +} + +// GetBlockTxids impl +func (b *Bridge) GetBlockTxids(blockHash string) ([]string, error) { + return electrs.GetBlockTxids(b, blockHash) +} + +// GetBlock impl +func (b *Bridge) GetBlock(blockHash string) (*electrs.ElectBlock, error) { + return electrs.GetBlock(b, blockHash) +} + +// GetBlockTransactions impl +func (b *Bridge) GetBlockTransactions(blockHash string, startIndex uint32) ([]*electrs.ElectTx, error) { + return electrs.GetBlockTransactions(b, blockHash, startIndex) +} + +// EstimateFeePerKb impl +func (b *Bridge) EstimateFeePerKb(blocks int) (int64, error) { + return electrs.EstimateFeePerKb(b, blocks) +} + +// GetBalance impl +func (b *Bridge) GetBalance(account string) (*big.Int, error) { + utxos, err := b.FindUtxos(account) + if err != nil { + return nil, err + } + var balance uint64 + for _, utxo := range utxos { + balance += *utxo.Value + } + return new(big.Int).SetUint64(balance), nil +} + +// GetTokenBalance impl +func (b *Bridge) GetTokenBalance(tokenType, tokenAddress, accountAddress string) (*big.Int, error) { + return nil, fmt.Errorf("[%v] can not get token balance of token with type '%v'", b.ChainConfig.BlockChain, tokenType) +} + +// GetTokenSupply impl +func (b *Bridge) GetTokenSupply(tokenType, tokenAddress string) (*big.Int, error) { + return nil, fmt.Errorf("[%v] can not get token supply of token with type '%v'", b.ChainConfig.BlockChain, tokenType) +} diff --git a/tokens/zol/electrs/callapi.go b/tokens/zol/electrs/callapi.go new file mode 100644 index 00000000..d8e095be --- /dev/null +++ b/tokens/zol/electrs/callapi.go @@ -0,0 +1,218 @@ +package electrs + +import ( + "fmt" + "sort" + + "github.com/anyswap/CrossChain-Bridge/rpc/client" + "github.com/anyswap/CrossChain-Bridge/tokens" +) + +// GetLatestBlockNumberOf call /blocks/tip/height +func GetLatestBlockNumberOf(apiAddress string) (uint64, error) { + var result uint64 + url := apiAddress + "/blocks/tip/height" + err := client.RPCGet(&result, url) + if err == nil { + return result, nil + } + return 0, err +} + +// GetLatestBlockNumber call /blocks/tip/height +func GetLatestBlockNumber(b tokens.CrossChainBridge) (result uint64, err error) { + gateway := b.GetGatewayConfig() + for _, apiAddress := range gateway.APIAddress { + url := apiAddress + "/blocks/tip/height" + err = client.RPCGet(&result, url) + if err == nil { + return result, nil + } + } + return 0, err +} + +// GetTransactionByHash call /tx/{txHash} +func GetTransactionByHash(b tokens.CrossChainBridge, txHash string) (*ElectTx, error) { + gateway := b.GetGatewayConfig() + var result ElectTx + var err error + for _, apiAddress := range gateway.APIAddress { + url := apiAddress + "/tx/" + txHash + err = client.RPCGet(&result, url) + if err == nil { + return &result, nil + } + } + return nil, err +} + +// GetElectTransactionStatus call /tx/{txHash}/status +func GetElectTransactionStatus(b tokens.CrossChainBridge, txHash string) (*ElectTxStatus, error) { + gateway := b.GetGatewayConfig() + var result ElectTxStatus + var err error + for _, apiAddress := range gateway.APIAddress { + url := apiAddress + "/tx/" + txHash + "/status" + err = client.RPCGet(&result, url) + if err == nil { + return &result, nil + } + } + return nil, err +} + +// FindUtxos call /address/{add}/utxo (confirmed first, then big value first) +func FindUtxos(b tokens.CrossChainBridge, addr string) (result []*ElectUtxo, err error) { + gateway := b.GetGatewayConfig() + for _, apiAddress := range gateway.APIAddress { + url := apiAddress + "/address/" + addr + "/utxo" + err = client.RPCGet(&result, url) + if err == nil { + sort.Sort(SortableElectUtxoSlice(result)) + return result, nil + } + } + return nil, err +} + +// GetPoolTxidList call /mempool/txids +func GetPoolTxidList(b tokens.CrossChainBridge) (result []string, err error) { + gateway := b.GetGatewayConfig() + for _, apiAddress := range gateway.APIAddress { + url := apiAddress + "/mempool/txids" + err = client.RPCGet(&result, url) + if err == nil { + return result, nil + } + } + return nil, err +} + +// GetPoolTransactions call /address/{addr}/txs/mempool +func GetPoolTransactions(b tokens.CrossChainBridge, addr string) (result []*ElectTx, err error) { + gateway := b.GetGatewayConfig() + for _, apiAddress := range gateway.APIAddress { + url := apiAddress + "/address/" + addr + "/txs/mempool" + err = client.RPCGet(&result, url) + if err == nil { + return result, nil + } + } + return nil, err +} + +// GetTransactionHistory call /address/{addr}/txs/chain +func GetTransactionHistory(b tokens.CrossChainBridge, addr, lastSeenTxid string) (result []*ElectTx, err error) { + gateway := b.GetGatewayConfig() + for _, apiAddress := range gateway.APIAddress { + url := apiAddress + "/address/" + addr + "/txs/chain" + if lastSeenTxid != "" { + url += "/" + lastSeenTxid + } + err = client.RPCGet(&result, url) + if err == nil { + return result, nil + } + } + return nil, err +} + +// GetOutspend call /tx/{txHash}/outspend/{vout} +func GetOutspend(b tokens.CrossChainBridge, txHash string, vout uint32) (*ElectOutspend, error) { + gateway := b.GetGatewayConfig() + var result ElectOutspend + var err error + for _, apiAddress := range gateway.APIAddress { + url := apiAddress + "/tx/" + txHash + "/outspend/" + fmt.Sprintf("%d", vout) + err = client.RPCGet(&result, url) + if err == nil { + return &result, nil + } + } + return nil, err +} + +// PostTransaction call post to /tx +func PostTransaction(b tokens.CrossChainBridge, txHex string) (txHash string, err error) { + gateway := b.GetGatewayConfig() + for _, apiAddress := range gateway.APIAddress { + url := apiAddress + "/tx" + txHash, err = client.RPCRawPost(url, txHex) + if err == nil { + return txHash, nil + } + } + return "", err +} + +// GetBlockHash call /block-height/{height} +func GetBlockHash(b tokens.CrossChainBridge, height uint64) (blockHash string, err error) { + gateway := b.GetGatewayConfig() + for _, apiAddress := range gateway.APIAddress { + url := apiAddress + "/block-height/" + fmt.Sprintf("%d", height) + blockHash, err = client.RPCRawGet(url) + if err == nil { + return blockHash, nil + } + } + return "", err +} + +// GetBlockTxids call /block/{blockHash}/txids +func GetBlockTxids(b tokens.CrossChainBridge, blockHash string) (result []string, err error) { + gateway := b.GetGatewayConfig() + for _, apiAddress := range gateway.APIAddress { + url := apiAddress + "/block/" + blockHash + "/txids" + err = client.RPCGet(&result, url) + if err == nil { + return result, nil + } + } + return nil, err +} + +// GetBlock call /block/{blockHash} +func GetBlock(b tokens.CrossChainBridge, blockHash string) (*ElectBlock, error) { + gateway := b.GetGatewayConfig() + var result ElectBlock + var err error + for _, apiAddress := range gateway.APIAddress { + url := apiAddress + "/block/" + blockHash + err = client.RPCGet(&result, url) + if err == nil { + return &result, nil + } + } + return nil, err +} + +// GetBlockTransactions call /block/{blockHash}/txs[/:start_index] (should start_index%25 == 0) +func GetBlockTransactions(b tokens.CrossChainBridge, blockHash string, startIndex uint32) (result []*ElectTx, err error) { + gateway := b.GetGatewayConfig() + for _, apiAddress := range gateway.APIAddress { + url := apiAddress + "/block/" + blockHash + "/txs/" + fmt.Sprintf("%d", startIndex) + err = client.RPCGet(&result, url) + if err == nil { + return result, nil + } + } + return nil, err +} + +// EstimateFeePerKb call /fee-estimates and multiply 1000 +func EstimateFeePerKb(b tokens.CrossChainBridge, blocks int) (fee int64, err error) { + var result map[int]float64 + gateway := b.GetGatewayConfig() + for _, apiAddress := range gateway.APIAddress { + url := apiAddress + "/fee-estimates" + err = client.RPCGet(&result, url) + if err == nil { + break + } + } + if err != nil { + return 0, err + } + return int64(result[blocks] * 1000), nil +} diff --git a/tokens/zol/electrs/types.go b/tokens/zol/electrs/types.go new file mode 100644 index 00000000..500ae0e1 --- /dev/null +++ b/tokens/zol/electrs/types.go @@ -0,0 +1,109 @@ +package electrs + +import ( + "fmt" +) + +// ElectBlock struct +type ElectBlock struct { + Hash *string `json:"id"` + Height *uint32 `json:"height"` + Version *uint32 `json:"version"` + Timestamp *uint32 `json:"timestamp"` + TxCount *uint32 `json:"tx_count"` + Size *uint32 `json:"size"` + Weight *uint32 `json:"weight"` + MerkleRoot *string `json:"merkle_root"` + PreviousHash *string `json:"previousblockhash"` + Nonce *uint32 `json:"nonce"` + Bits *uint32 `json:"bits"` + Difficulty *uint64 `json:"difficulty"` +} + +// ElectTx struct +type ElectTx struct { + Txid *string `json:"txid"` + Version *uint32 `json:"version"` + Locktime *uint32 `json:"locktime"` + Size *uint32 `json:"size"` + Weight *uint32 `json:"weight"` + Fee *uint64 `json:"fee"` + Vin []*ElectTxin `json:"vin"` + Vout []*ElectTxOut `json:"vout"` + Status *ElectTxStatus `json:"status,omitempty"` +} + +// ElectTxin struct +type ElectTxin struct { + Txid *string `json:"txid"` + Vout *uint32 `json:"vout"` + Scriptsig *string `json:"scriptsig"` + ScriptsigAsm *string `json:"scriptsig_asm"` + IsCoinbase *bool `json:"is_coinbase"` + Sequence *uint32 `json:"sequence"` + InnerRedeemscriptAsm *string `json:"inner_redeemscript_asm"` + Prevout *ElectTxOut `json:"prevout"` +} + +// ElectTxOut struct +type ElectTxOut struct { + Scriptpubkey *string `json:"scriptpubkey"` + ScriptpubkeyAsm *string `json:"scriptpubkey_asm"` + ScriptpubkeyType *string `json:"scriptpubkey_type"` + ScriptpubkeyAddress *string `json:"scriptpubkey_address"` + Value *uint64 `json:"value"` +} + +// ElectOutspend struct +type ElectOutspend struct { + Spent *bool `json:"spent"` + Txid *string `json:"txid"` + Vin *uint32 `json:"vin"` + Status *ElectTxStatus `json:"status,omitempty"` +} + +// ElectTxStatus struct +type ElectTxStatus struct { + Confirmed *bool `json:"confirmed"` + BlockHeight *uint64 `json:"block_height"` + BlockHash *string `json:"block_hash"` + BlockTime *uint64 `json:"block_time"` +} + +// ElectUtxo struct +type ElectUtxo struct { + Txid *string `json:"txid"` + Vout *uint32 `json:"vout"` + Value *uint64 `json:"value"` + Status *ElectTxStatus `json:"status"` +} + +func (utxo *ElectUtxo) String() string { + return fmt.Sprintf("txid %v vout %v value %v confirmed %v", *utxo.Txid, *utxo.Vout, *utxo.Value, *utxo.Status.Confirmed) +} + +// SortableElectUtxoSlice sortable +type SortableElectUtxoSlice []*ElectUtxo + +// Len impl Sortable +func (s SortableElectUtxoSlice) Len() int { + return len(s) +} + +// Swap impl Sortable +func (s SortableElectUtxoSlice) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// Less impl Sortable +// sort utxos +// 1. confirmed fisrt +// 2. value first +func (s SortableElectUtxoSlice) Less(i, j int) bool { + confirmed1 := *s[i].Status.Confirmed + confirmed2 := *s[j].Status.Confirmed + if confirmed1 != confirmed2 { + return confirmed1 + } + return *s[i].Value > *s[j].Value +} diff --git a/tokens/zol/init.go b/tokens/zol/init.go new file mode 100644 index 00000000..0f2be0c0 --- /dev/null +++ b/tokens/zol/init.go @@ -0,0 +1,108 @@ +package zol + +import ( + "github.com/anyswap/CrossChain-Bridge/log" + "github.com/anyswap/CrossChain-Bridge/tokens" +) + +var ( + cfgMinRelayFee int64 = 400 + cfgMinRelayFeePerKb int64 = 2000 + cfgMaxRelayFeePerKb int64 = 500000 + cfgPlusFeePercentage uint64 = 0 + cfgEstimateFeeBlocks = 6 + + cfgFromPublicKey string + + cfgUtxoAggregateMinCount = 20 + cfgUtxoAggregateMinValue = uint64(1000000) + cfgUtxoAggregateToAddress string +) + +// Init init btc extra +func Init(btcExtra *tokens.BtcExtraConfig) { + if BridgeInstance == nil { + return + } + + if btcExtra == nil { + log.Fatal("Zerolimit bridge must config 'BtcExtra'") + } + + initFromPublicKey() + initRelayFee(btcExtra) + initAggregate(btcExtra) +} + +func initFromPublicKey() { + if len(tokens.GetTokenPairsConfig()) != 1 { + log.Fatalf("Zerolimit bridge does not support multiple tokens") + } + + pairCfg, exist := tokens.GetTokenPairsConfig()[PairID] + if !exist { + log.Fatalf("Zerolimit bridge must have pairID %v", PairID) + } + + cfgFromPublicKey = pairCfg.SrcToken.DcrmPubkey + _, err := BridgeInstance.GetCompressedPublicKey(cfgFromPublicKey, true) + if err != nil { + log.Fatal("wrong btc dcrm public key", "err", err) + } +} + +func initRelayFee(btcExtra *tokens.BtcExtraConfig) { + if btcExtra.MinRelayFee > 0 { + cfgMinRelayFee = btcExtra.MinRelayFee + maxMinRelayFee, _ := newAmount(0.001) + minRelayFee := btcAmountType(cfgMinRelayFee) + if minRelayFee > maxMinRelayFee { + log.Fatal("BtcMinRelayFee is too large", "value", minRelayFee, "max", maxMinRelayFee) + } + } + + if btcExtra.EstimateFeeBlocks > 0 { + cfgEstimateFeeBlocks = btcExtra.EstimateFeeBlocks + if cfgEstimateFeeBlocks > 25 { + log.Fatal("EstimateFeeBlocks is too large, must <= 25") + } + } + + if btcExtra.PlusFeePercentage > 0 { + cfgPlusFeePercentage = btcExtra.PlusFeePercentage + if cfgPlusFeePercentage > 5000 { + log.Fatal("PlusFeePercentage is too large, must <= 5000") + } + } + + if btcExtra.MaxRelayFeePerKb > 0 { + cfgMaxRelayFeePerKb = btcExtra.MaxRelayFeePerKb + } + + if btcExtra.MinRelayFeePerKb > 0 { + cfgMinRelayFeePerKb = btcExtra.MinRelayFeePerKb + } + + if cfgMinRelayFeePerKb > cfgMaxRelayFeePerKb { + log.Fatal("MinRelayFeePerKb is larger than MaxRelayFeePerKb", "min", cfgMinRelayFeePerKb, "max", cfgMaxRelayFeePerKb) + } + + log.Info("Init Btc extra", "MinRelayFee", cfgMinRelayFee, "MinRelayFeePerKb", cfgMinRelayFeePerKb, "MaxRelayFeePerKb", cfgMaxRelayFeePerKb, "PlusFeePercentage", cfgPlusFeePercentage) +} + +func initAggregate(btcExtra *tokens.BtcExtraConfig) { + if btcExtra.UtxoAggregateMinCount > 0 { + cfgUtxoAggregateMinCount = btcExtra.UtxoAggregateMinCount + } + + if btcExtra.UtxoAggregateMinValue > 0 { + cfgUtxoAggregateMinValue = btcExtra.UtxoAggregateMinValue + } + + cfgUtxoAggregateToAddress = btcExtra.UtxoAggregateToAddress + if !BridgeInstance.IsValidAddress(cfgUtxoAggregateToAddress) { + log.Fatal("wrong utxo aggregate to address", "toAddress", cfgUtxoAggregateToAddress) + } + + log.Info("Init Btc extra", "UtxoAggregateMinCount", cfgUtxoAggregateMinCount, "UtxoAggregateMinValue", cfgUtxoAggregateMinValue, "UtxoAggregateToAddress", cfgUtxoAggregateToAddress) +} diff --git a/tokens/zol/instance.go b/tokens/zol/instance.go new file mode 100644 index 00000000..5924db48 --- /dev/null +++ b/tokens/zol/instance.go @@ -0,0 +1,23 @@ +package zol + +import ( + "github.com/anyswap/CrossChain-Bridge/tokens" + "github.com/anyswap/CrossChain-Bridge/tokens/btc/electrs" +) + +// BridgeInstance btc bridge instance +var BridgeInstance BridgeInterface + +// BridgeInterface btc bridge interface +type BridgeInterface interface { + tokens.CrossChainBridge + + GetCompressedPublicKey(fromPublicKey string, needVerify bool) (cPkData []byte, err error) + GetP2shAddress(bindAddr string) (p2shAddress string, redeemScript []byte, err error) + VerifyP2shTransaction(pairID, txHash, bindAddress string, allowUnstable bool) (*tokens.TxSwapInfo, error) + VerifyAggregateMsgHash(msgHash []string, args *tokens.BuildTxArgs) error + AggregateUtxos(addrs []string, utxos []*electrs.ElectUtxo) (string, error) + FindUtxos(addr string) ([]*electrs.ElectUtxo, error) + StartSwapHistoryScanJob() + ShouldAggregate(aggUtxoCount int, aggSumVal uint64) bool +} diff --git a/tokens/zol/p2shaddress.go b/tokens/zol/p2shaddress.go new file mode 100644 index 00000000..84b3a5ae --- /dev/null +++ b/tokens/zol/p2shaddress.go @@ -0,0 +1,83 @@ +package zol + +import ( + "fmt" + + "github.com/anyswap/CrossChain-Bridge/common" + "github.com/anyswap/CrossChain-Bridge/tokens" + "github.com/anyswap/CrossChain-Bridge/tokens/tools" +) + +func (b *Bridge) getP2shAddressWithMemo(memo, pubKeyHash []byte) (p2shAddress string, redeemScript []byte, err error) { + redeemScript, err = b.GetP2shRedeemScript(memo, pubKeyHash) + if err != nil { + return + } + addressScriptHash, err := b.NewAddressScriptHash(redeemScript) + if err != nil { + return + } + p2shAddress = addressScriptHash.EncodeAddress() + return +} + +// GetP2shAddress get p2sh address from bind address +func (b *Bridge) GetP2shAddress(bindAddr string) (p2shAddress string, redeemScript []byte, err error) { + if !tokens.GetCrossChainBridge(!b.IsSrc).IsValidAddress(bindAddr) { + return "", nil, fmt.Errorf("invalid bind address %v", bindAddr) + } + memo := common.FromHex(bindAddr) + pairID := PairID + tokenCfg := b.GetTokenConfig(pairID) + if tokenCfg == nil { + return "", nil, tokens.ErrUnknownPairID + } + + dcrmAddress := tokenCfg.DcrmAddress + address, err := b.DecodeAddress(dcrmAddress) + if err != nil { + return "", nil, fmt.Errorf("invalid dcrm address %v, %v", dcrmAddress, err) + } + pubKeyHash := address.ScriptAddress() + return b.getP2shAddressWithMemo(memo, pubKeyHash) +} + +func (b *Bridge) getRedeemScriptByOutputScrpit(preScript []byte) ([]byte, error) { + pkScript, err := b.ParsePkScript(preScript) + if err != nil { + return nil, err + } + p2shAddress, err := pkScript.Address(b.Inherit.GetChainParams()) + if err != nil { + return nil, err + } + p2shAddr := p2shAddress.String() + bindAddr := tools.GetP2shBindAddress(p2shAddr) + if bindAddr == "" { + return nil, fmt.Errorf("ps2h address %v is registered", p2shAddr) + } + var address string + address, redeemScript, _ := b.GetP2shAddress(bindAddr) + if address != p2shAddr { + return nil, fmt.Errorf("ps2h address mismatch for bind address %v, have %v want %v", bindAddr, p2shAddr, address) + } + return redeemScript, nil +} + +// GetP2shAddressByRedeemScript get p2sh address by redeem script +func (b *Bridge) GetP2shAddressByRedeemScript(redeemScript []byte) (string, error) { + addressScriptHash, err := b.NewAddressScriptHash(redeemScript) + if err != nil { + return "", err + } + return addressScriptHash.EncodeAddress(), nil +} + +// GetP2shSigScript get p2sh signature script +func (b *Bridge) GetP2shSigScript(redeemScript []byte) ([]byte, error) { + p2shAddr, err := b.GetP2shAddressByRedeemScript(redeemScript) + if err != nil { + return nil, err + } + return b.GetPayToAddrScript(p2shAddr) +} diff --git a/tokens/zol/printtx.go b/tokens/zol/printtx.go new file mode 100644 index 00000000..2e4e1acd --- /dev/null +++ b/tokens/zol/printtx.go @@ -0,0 +1,113 @@ +package zol + +import ( + "bytes" + "encoding/json" + + "github.com/anyswap/CrossChain-Bridge/common/hexutil" + "github.com/btcsuite/btcwallet/wallet/txauthor" +) + +// MarshalToJSON marshal to json +func MarshalToJSON(obj interface{}, pretty bool) string { + var jsdata []byte + if pretty { + jsdata, _ = json.MarshalIndent(obj, "", " ") + } else { + jsdata, _ = json.Marshal(obj) + } + return string(jsdata) +} + +// AuthoredTxToString AuthoredTx to string +func AuthoredTxToString(authtx interface{}, pretty bool) string { + authoredTx, ok := authtx.(*txauthor.AuthoredTx) + if !ok { + return MarshalToJSON(authtx, pretty) + } + + var encAuthTx EncAuthoredTx + + encAuthTx.ChangeIndex = authoredTx.ChangeIndex + encAuthTx.TotalInput = authoredTx.TotalInput + + tx := authoredTx.Tx + if tx == nil { + return MarshalToJSON(encAuthTx, pretty) + } + + buf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize())) + _ = tx.Serialize(buf) + txid := tx.TxHash().String() + + var encTx EncMsgTx + + encTx.Txid = txid + encTx.Version = tx.Version + encTx.LockTime = tx.LockTime + + encTx.TxOut = make([]*EncTxOut, len(tx.TxOut)) + for i, txOut := range tx.TxOut { + encTx.TxOut[i] = &EncTxOut{ + Value: txOut.Value, + } + encTx.TxOut[i].PkScript, _ = disasmScriptToString(txOut.PkScript) + } + + encTx.TxIn = make([]*EncTxIn, len(tx.TxIn)) + for i, txIn := range tx.TxIn { + encTx.TxIn[i] = &EncTxIn{ + PreviousOutPoint: EncOutPoint{ + Hash: txIn.PreviousOutPoint.Hash.String(), + Index: txIn.PreviousOutPoint.Index, + }, + Sequence: txIn.Sequence, + Value: authoredTx.PrevInputValues[i], + } + encTx.TxIn[i].SignatureScript, _ = disasmScriptToString(txIn.SignatureScript) + encTx.TxIn[i].Witness = make([]hexutil.Bytes, len(txIn.Witness)) + for j, witness := range txIn.Witness { + encTx.TxIn[i].Witness[j] = hexutil.Bytes(witness) + } + } + + encAuthTx.Tx = &encTx + return MarshalToJSON(encAuthTx, pretty) +} + +// EncAuthoredTx stuct +type EncAuthoredTx struct { + Tx *EncMsgTx + TotalInput btcAmountType + ChangeIndex int +} + +// EncMsgTx struct +type EncMsgTx struct { + Txid string + Version int32 + TxIn []*EncTxIn + TxOut []*EncTxOut + LockTime uint32 +} + +// EncTxOut struct +type EncTxOut struct { + PkScript string + Value int64 +} + +// EncOutPoint struct +type EncOutPoint struct { + Hash string + Index uint32 +} + +// EncTxIn struct +type EncTxIn struct { + PreviousOutPoint EncOutPoint + SignatureScript string + Witness []hexutil.Bytes + Sequence uint32 + Value btcAmountType +} diff --git a/tokens/zol/processtx.go b/tokens/zol/processtx.go new file mode 100644 index 00000000..9397bbba --- /dev/null +++ b/tokens/zol/processtx.go @@ -0,0 +1,111 @@ +package zol + +import ( + "fmt" + + "github.com/anyswap/CrossChain-Bridge/log" + "github.com/anyswap/CrossChain-Bridge/tokens" + "github.com/anyswap/CrossChain-Bridge/tokens/btc/electrs" + "github.com/anyswap/CrossChain-Bridge/tokens/tools" +) + +func (b *Bridge) processTransaction(txid string) { + var tx *electrs.ElectTx + var err error + for i := 0; i < 2; i++ { + tx, err = b.GetTransactionByHash(txid) + if err == nil { + break + } + } + if err != nil { + log.Debug("[processTransaction] "+b.ChainConfig.BlockChain+" Bridge::GetTransaction fail", "tx", txid, "err", err) + return + } + b.processTransactionImpl(tx) +} + +func (b *Bridge) processTransactionImpl(tx *electrs.ElectTx) { + p2shBindAddrs, err := b.CheckSwapinTxType(tx) + if err != nil { + return + } + txid := *tx.Txid + if len(p2shBindAddrs) > 0 { + for _, p2shBindAddr := range p2shBindAddrs { + b.processP2shSwapin(txid, p2shBindAddr) + } + } else { + b.processSwapin(txid) + } +} + +func (b *Bridge) processSwapin(txid string) { + if tools.IsSwapExist(txid, PairID, "", true) { + return + } + swapInfo, err := b.verifySwapinTx(PairID, txid, true) + tools.RegisterSwapin(txid, []*tokens.TxSwapInfo{swapInfo}, []error{err}) +} + +func (b *Bridge) processP2shSwapin(txid, bindAddress string) { + if tools.IsSwapExist(txid, PairID, bindAddress, true) { + return + } + swapInfo, err := b.verifyP2shSwapinTx(PairID, txid, bindAddress, true) + tools.RegisterP2shSwapin(txid, swapInfo, err) +} + +func isP2pkhSwapinPrior(tx *electrs.ElectTx, depositAddress string) bool { + txFrom := getTxFrom(tx.Vin, depositAddress) + if txFrom == depositAddress { + return false + } + var memoScript string + for i := len(tx.Vout) - 1; i >= 0; i-- { // reverse iterate + output := tx.Vout[i] + if *output.ScriptpubkeyType == opReturnType { + memoScript = *output.ScriptpubkeyAsm + break + } + } + bindAddress, bindOk := GetBindAddressFromMemoScipt(memoScript) + return bindOk && tokens.DstBridge.IsValidAddress(bindAddress) +} + +// CheckSwapinTxType check swapin type +func (b *Bridge) CheckSwapinTxType(tx *electrs.ElectTx) (p2shBindAddrs []string, err error) { + tokenCfg := b.GetTokenConfig(PairID) + if tokenCfg == nil { + return nil, fmt.Errorf("swap pair '%v' is not configed", PairID) + } + depositAddress := tokenCfg.DepositAddress + p2pkhSwapinPrior := isP2pkhSwapinPrior(tx, depositAddress) + p2shAddressMap := make(map[string]struct{}) + for _, output := range tx.Vout { + if output.ScriptpubkeyAddress == nil { + continue + } + switch *output.ScriptpubkeyType { + case p2shType: + // use the first registered p2sh address + p2shAddress := *output.ScriptpubkeyAddress + if _, exist := p2shAddressMap[p2shAddress]; exist { + continue + } + p2shAddressMap[p2shAddress] = struct{}{} + p2shBindAddr := tools.GetP2shBindAddress(p2shAddress) + if p2shBindAddr != "" { + p2shBindAddrs = append(p2shBindAddrs, p2shBindAddr) + } + case p2pkhType: + if p2pkhSwapinPrior && *output.ScriptpubkeyAddress == depositAddress { + return nil, nil // use p2pkh if exist + } + } + } + if len(p2shBindAddrs) > 0 { + return p2shBindAddrs, nil + } + return nil, tokens.ErrTxWithWrongReceiver +} diff --git a/tokens/zol/scanchaintx.go b/tokens/zol/scanchaintx.go new file mode 100644 index 00000000..6a22ed96 --- /dev/null +++ b/tokens/zol/scanchaintx.go @@ -0,0 +1,96 @@ +package zol + +import ( + "fmt" + "time" + + "github.com/anyswap/CrossChain-Bridge/log" + "github.com/anyswap/CrossChain-Bridge/tokens/tools" +) + +var ( + scannedBlocks = tools.NewCachedScannedBlocks(13) + + maxScanHeight = uint64(100) + retryIntervalInScanJob = 3 * time.Second + restIntervalInScanJob = 3 * time.Second +) + +func (b *Bridge) getStartAndLatestHeight() (start, latest uint64) { + startHeight := tools.GetLatestScanHeight(b.IsSrc) + + chainCfg := b.GetChainConfig() + confirmations := *chainCfg.Confirmations + initialHeight := *chainCfg.InitialHeight + + latest = tools.LoopGetLatestBlockNumber(b) + + switch { + case startHeight != 0: + start = startHeight + case initialHeight != 0: + start = initialHeight + default: + if latest > confirmations { + start = latest - confirmations + } + } + if start < initialHeight { + start = initialHeight + } + if start+maxScanHeight < latest { + start = latest - maxScanHeight + } + return start, latest +} + +// StartChainTransactionScanJob scan job +func (b *Bridge) StartChainTransactionScanJob() { + go b.StartPoolTransactionScanJob() + + chainName := b.ChainConfig.BlockChain + log.Infof("[scanchain] start %v scan chain job", chainName) + + start, latest := b.getStartAndLatestHeight() + _ = tools.UpdateLatestScanInfo(b.IsSrc, start) + log.Infof("[scanchain] start %v scan chain loop from %v latest=%v", chainName, start, latest) + + chainCfg := b.GetChainConfig() + confirmations := *chainCfg.Confirmations + + stable := start + errorSubject := fmt.Sprintf("[scanchain] get %v block failed", chainName) + scanSubject := fmt.Sprintf("[scanchain] scanned %v block", chainName) + for { + latest := tools.LoopGetLatestBlockNumber(b) + for h := stable + 1; h <= latest; { + blockHash, err := b.GetBlockHash(h) + if err != nil { + log.Error(errorSubject, "height", h, "err", err) + time.Sleep(retryIntervalInScanJob) + continue + } + if scannedBlocks.IsBlockScanned(blockHash) { + h++ + continue + } + txids, err := b.GetBlockTxids(blockHash) + if err != nil { + log.Error(errorSubject, "height", h, "blockHash", blockHash, "err", err) + time.Sleep(retryIntervalInScanJob) + continue + } + for _, txid := range txids { + b.processTransaction(txid) + } + scannedBlocks.CacheScannedBlock(blockHash, h) + log.Info(scanSubject, "blockHash", blockHash, "height", h, "txs", len(txids)) + h++ + } + if stable+confirmations < latest { + stable = latest - confirmations + _ = tools.UpdateLatestScanInfo(b.IsSrc, stable) + } + time.Sleep(restIntervalInScanJob) + } +} diff --git a/tokens/zol/scanpooltx.go b/tokens/zol/scanpooltx.go new file mode 100644 index 00000000..b28f9071 --- /dev/null +++ b/tokens/zol/scanpooltx.go @@ -0,0 +1,38 @@ +package zol + +import ( + "fmt" + "time" + + "github.com/anyswap/CrossChain-Bridge/log" + "github.com/anyswap/CrossChain-Bridge/tokens/tools" +) + +var ( + scannedTxs = tools.NewCachedScannedTxs(3000) +) + +// StartPoolTransactionScanJob scan job +func (b *Bridge) StartPoolTransactionScanJob() { + chainName := b.ChainConfig.BlockChain + log.Infof("[scanpool] start scan %v tx pool job", chainName) + errorSubject := fmt.Sprintf("[scanpool] get %v pool txs error", chainName) + scanSubject := fmt.Sprintf("[scanpool] scanned %v tx", chainName) + for { + txids, err := b.GetPoolTxidList() + if err != nil { + log.Error(errorSubject, "err", err) + time.Sleep(retryIntervalInScanJob) + continue + } + for _, txid := range txids { + if scannedTxs.IsTxScanned(txid) { + continue + } + log.Trace(scanSubject, "txid", txid) + b.processTransaction(txid) + scannedTxs.CacheScannedTx(txid) + } + time.Sleep(restIntervalInScanJob) + } +} diff --git a/tokens/zol/scanswaphistory.go b/tokens/zol/scanswaphistory.go new file mode 100644 index 00000000..f96ec5a7 --- /dev/null +++ b/tokens/zol/scanswaphistory.go @@ -0,0 +1,127 @@ +package zol + +import ( + "fmt" + "time" + + "github.com/anyswap/CrossChain-Bridge/log" + "github.com/anyswap/CrossChain-Bridge/tokens/tools" +) + +var ( + maxFirstScanHeight = uint64(1000) + + firstScannedTxs = tools.NewCachedScannedTxs(500) + historyScannedTxs = tools.NewCachedScannedTxs(500) +) + +// StartSwapHistoryScanJob scan job +func (b *Bridge) StartSwapHistoryScanJob() { + log.Infof("[swaphistory] start scan %v swap history job", b.ChainConfig.BlockChain) + + go b.scanFirstLoop() + + b.scanTransactionHistory() +} + +func (b *Bridge) scanFirstLoop() { + // first loop process all tx history no matter whether processed before + latest := tools.LoopGetLatestBlockNumber(b) + minHeight := *b.ChainConfig.InitialHeight + if minHeight+maxFirstScanHeight < latest { + minHeight = latest - maxFirstScanHeight + } + chainName := b.ChainConfig.BlockChain + log.Infof("[scanFirstLoop] start %v first scan loop to min height %v", chainName, minHeight) + + tokenCfg := b.GetTokenConfig(PairID) + lastSeenTxid := "" + +FIRST_LOOP: + for { + txHistory, err := b.GetTransactionHistory(tokenCfg.DepositAddress, lastSeenTxid) + if err != nil { + time.Sleep(retryIntervalInScanJob) + continue + } + if len(txHistory) == 0 { + break + } + for _, tx := range txHistory { + if tx.Status.BlockHeight == nil { + continue + } + height := *tx.Status.BlockHeight + if height < minHeight { + break FIRST_LOOP + } + txid := *tx.Txid + if !firstScannedTxs.IsTxScanned(txid) { + log.Tracef("[scanFirstLoop] process %v tx. txid=%v height=%v", chainName, txid, height) + b.processSwapin(txid) + firstScannedTxs.CacheScannedTx(txid) + } + } + lastSeenTxid = *txHistory[len(txHistory)-1].Txid + } + + log.Infof("[scanFirstLoop] finish %v first scan loop to min height %v", chainName, minHeight) +} + +func (b *Bridge) scanTransactionHistory() { + var ( + lastSeenTxid = "" + rescan = true + ) + + latest := tools.LoopGetLatestBlockNumber(b) + minHeight := *b.ChainConfig.InitialHeight + if minHeight+maxScanHeight < latest { + minHeight = latest - maxScanHeight + } + + chainName := b.ChainConfig.BlockChain + errorSubject := fmt.Sprintf("[scanhistory] get %v tx history failed", chainName) + scanSubject := fmt.Sprintf("[scanhistory] scanned %v tx", chainName) + log.Infof("[scanhistory] start %v scan swap history loop from height %v", chainName, minHeight) + + tokenCfg := b.GetTokenConfig(PairID) + + for { + txHistory, err := b.GetTransactionHistory(tokenCfg.DepositAddress, lastSeenTxid) + if err != nil { + log.Error(errorSubject, "err", err) + time.Sleep(retryIntervalInScanJob) + continue + } + if len(txHistory) == 0 { + rescan = true + } else if rescan { + rescan = false + } + for _, tx := range txHistory { + if tx.Status.BlockHeight == nil { + continue + } + height := *tx.Status.BlockHeight + if height < minHeight { + rescan = true + break + } + txid := *tx.Txid + if !historyScannedTxs.IsTxScanned(txid) { + rescan = true + break // rescan if already processed + } + log.Trace(scanSubject, "txid", txid, "height", height) + b.processSwapin(txid) + historyScannedTxs.CacheScannedTx(txid) + } + if rescan { + lastSeenTxid = "" + time.Sleep(restIntervalInScanJob) + } else { + lastSeenTxid = *txHistory[len(txHistory)-1].Txid + } + } +} diff --git a/tokens/zol/sendtx.go b/tokens/zol/sendtx.go new file mode 100644 index 00000000..7d8c2dfe --- /dev/null +++ b/tokens/zol/sendtx.go @@ -0,0 +1,33 @@ +package zol + +import ( + "bytes" + "encoding/hex" + + "github.com/anyswap/CrossChain-Bridge/log" + "github.com/anyswap/CrossChain-Bridge/tokens" + "github.com/btcsuite/btcwallet/wallet/txauthor" +) + +// SendTransaction send signed tx +func (b *Bridge) SendTransaction(signedTx interface{}) (txHash string, err error) { + authoredTx, ok := signedTx.(*txauthor.AuthoredTx) + if !ok { + return "", tokens.ErrWrongRawTx + } + + tx := authoredTx.Tx + if tx == nil { + return "", tokens.ErrWrongRawTx + } + + buf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize())) + err = tx.Serialize(buf) + if err != nil { + return "", err + } + txHex := hex.EncodeToString(buf.Bytes()) + log.Info("Bridge send tx", "hash", tx.TxHash()) + + return b.PostTransaction(txHex) +} diff --git a/tokens/zol/signtx.go b/tokens/zol/signtx.go new file mode 100644 index 00000000..95103501 --- /dev/null +++ b/tokens/zol/signtx.go @@ -0,0 +1,361 @@ +package zol + +import ( + "bytes" + "crypto/ecdsa" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "math/big" + "time" + + "github.com/anyswap/CrossChain-Bridge/common" + "github.com/anyswap/CrossChain-Bridge/dcrm" + "github.com/anyswap/CrossChain-Bridge/log" + "github.com/anyswap/CrossChain-Bridge/tokens" + "github.com/anyswap/CrossChain-Bridge/tools/crypto" + "github.com/btcsuite/btcwallet/wallet/txauthor" +) + +const ( + retryGetSignStatusCount = 70 + retryGetSignStatusInterval = 10 * time.Second +) + +func (b *Bridge) verifyTransactionWithArgs(tx *txauthor.AuthoredTx, args *tokens.BuildTxArgs) error { + checkReceiver := args.Bind + if args.Identifier == tokens.AggregateIdentifier { + checkReceiver = cfgUtxoAggregateToAddress + } + payToReceiverScript, err := b.GetPayToAddrScript(checkReceiver) + if err != nil { + return err + } + isRightReceiver := false + for _, out := range tx.Tx.TxOut { + if bytes.Equal(out.PkScript, payToReceiverScript) { + isRightReceiver = true + break + } + } + if !isRightReceiver { + return fmt.Errorf("[sign] verify tx receiver failed") + } + return nil +} + +// DcrmSignTransaction dcrm sign raw tx +func (b *Bridge) DcrmSignTransaction(rawTx interface{}, args *tokens.BuildTxArgs) (signedTx interface{}, txHash string, err error) { + authoredTx, ok := rawTx.(*txauthor.AuthoredTx) + if !ok { + return nil, "", tokens.ErrWrongRawTx + } + + err = b.verifyTransactionWithArgs(authoredTx, args) + if err != nil { + return nil, "", err + } + + cPkData, err := b.GetCompressedPublicKey(cfgFromPublicKey, false) + if err != nil { + return nil, "", err + } + + var ( + msgHashes []string + rsvs []string + sigScripts [][]byte + hasP2shInput bool + sigHash []byte + ) + + for i, preScript := range authoredTx.PrevScripts { + sigScript := preScript + if b.IsPayToScriptHash(preScript) { + sigScript, err = b.getRedeemScriptByOutputScrpit(preScript) + if err != nil { + return nil, "", err + } + hasP2shInput = true + } + + sigHash, err = b.CalcSignatureHash(sigScript, authoredTx.Tx, i) + if err != nil { + return nil, "", err + } + msgHash := hex.EncodeToString(sigHash) + msgHashes = append(msgHashes, msgHash) + sigScripts = append(sigScripts, sigScript) + } + if !hasP2shInput { + sigScripts = nil + } + + rsvs, err = b.DcrmSignMsgHash(msgHashes, args) + if err != nil { + return nil, "", err + } + + return b.MakeSignedTransaction(authoredTx, msgHashes, rsvs, sigScripts, cPkData) +} + +func checkEqualLength(authoredTx *txauthor.AuthoredTx, msgHash, rsv []string, sigScripts [][]byte) error { + txIn := authoredTx.Tx.TxIn + if len(txIn) != len(msgHash) { + return errors.New("mismatch number of msghashes and tx inputs") + } + if len(txIn) != len(rsv) { + return errors.New("mismatch number of signatures and tx inputs") + } + if sigScripts != nil && len(sigScripts) != len(txIn) { + return errors.New("mismatch number of signatures scripts and tx inputs") + } + return nil +} + +// MakeSignedTransaction make signed tx +func (b *Bridge) MakeSignedTransaction(authoredTx *txauthor.AuthoredTx, msgHash, rsv []string, sigScripts [][]byte, cPkData []byte) (signedTx interface{}, txHash string, err error) { + if len(cPkData) == 0 { + return nil, "", errors.New("empty public key data") + } + err = checkEqualLength(authoredTx, msgHash, rsv, sigScripts) + if err != nil { + return nil, "", err + } + log.Info(b.ChainConfig.BlockChain+" Bridge MakeSignedTransaction", "msghash", msgHash, "count", len(msgHash)) + + for i, txin := range authoredTx.Tx.TxIn { + signData, ok := b.getSigDataFromRSV(rsv[i]) + if !ok { + return nil, "", errors.New("wrong RSV data") + } + + sigScript, err := b.GetSigScript(sigScripts, authoredTx.PrevScripts[i], signData, cPkData, i) + if err != nil { + return nil, "", err + } + txin.SignatureScript = sigScript + } + txHash = authoredTx.Tx.TxHash().String() + log.Info(b.ChainConfig.BlockChain+" MakeSignedTransaction success", "txhash", txHash) + return authoredTx, txHash, nil +} + +// VerifyRedeemScript verify redeem script +func (b *Bridge) VerifyRedeemScript(prevScript, redeemScript []byte) error { + p2shScript, err := b.GetP2shSigScript(redeemScript) + if err != nil { + return err + } + if !bytes.Equal(p2shScript, prevScript) { + return fmt.Errorf("redeem script %x mismatch", redeemScript) + } + return nil +} + +func (b *Bridge) getSigDataFromRSV(rsv string) ([]byte, bool) { + rs := rsv[0 : len(rsv)-2] + + r := rs[:64] + s := rs[64:] + + rr, ok := new(big.Int).SetString(r, 16) + if !ok { + return nil, false + } + + ss, ok := new(big.Int).SetString(s, 16) + if !ok { + return nil, false + } + + return b.SerializeSignature(rr, ss), true +} + +func (b *Bridge) verifyPublickeyData(pkData []byte) error { + tokenCfg := b.GetTokenConfig(PairID) + if tokenCfg == nil { + return nil + } + dcrmAddress := tokenCfg.DcrmAddress + if dcrmAddress == "" { + return nil + } + address, err := b.NewAddressPubKeyHash(pkData) + if err != nil { + return err + } + if address.EncodeAddress() != dcrmAddress { + return fmt.Errorf("public key address %v is not the configed dcrm address %v", address, dcrmAddress) + } + return nil +} + +// GetCompressedPublicKey get compressed public key +func (b *Bridge) GetCompressedPublicKey(fromPublicKey string, needVerify bool) (cPkData []byte, err error) { + if fromPublicKey == "" { + return nil, nil + } + pkData := common.FromHex(fromPublicKey) + cPkData, err = b.ToCompressedPublicKey(pkData) + if err != nil { + return nil, err + } + if needVerify { + err = b.verifyPublickeyData(cPkData) + if err != nil { + return nil, err + } + } + return cPkData, nil +} + +// the rsv must have correct v (recovery id), otherwise will get wrong public key data. +func (b *Bridge) getPkDataFromSig(rsv, msgHash string, compressed bool) (pkData []byte, err error) { + rsvData := common.FromHex(rsv) + hashData := common.FromHex(msgHash) + ecPub, err := crypto.SigToPub(hashData, rsvData) + if err != nil { + return nil, err + } + return b.SerializePublicKey(ecPub, compressed), nil +} + +// DcrmSignMsgHash dcrm sign msg hash +func (b *Bridge) DcrmSignMsgHash(msgHash []string, args *tokens.BuildTxArgs) (rsv []string, err error) { + extra := args.Extra.BtcExtra + if extra == nil { + return nil, tokens.ErrWrongExtraArgs + } + jsondata, _ := json.Marshal(args) + msgContext := []string{string(jsondata)} + log.Info("=========="+b.ChainConfig.BlockChain+" DcrmSignTransaction start"+" ==========", "msgContext", msgContext) + rpcAddr, keyID, err := dcrm.DoSign(cfgFromPublicKey, msgHash, msgContext) + if err != nil { + return nil, err + } + log.Info(b.ChainConfig.BlockChain+" DcrmSignTransaction start", "keyID", keyID, "msghash", msgHash, "txid", args.SwapID) + time.Sleep(retryGetSignStatusInterval) + + var signStatus *dcrm.SignStatus + i := 0 + for ; i < retryGetSignStatusCount; i++ { + signStatus, err = dcrm.GetSignStatus(keyID, rpcAddr) + if err == nil { + if len(signStatus.Rsv) != len(msgHash) { + return nil, fmt.Errorf("get sign status require %v rsv but have %v (keyID = %v)", len(msgHash), len(signStatus.Rsv), keyID) + } + rsv = signStatus.Rsv + break + } + switch err { + case dcrm.ErrGetSignStatusFailed, dcrm.ErrGetSignStatusTimeout: + return nil, err + } + log.Warn("retry get sign status as error", "err", err, "txid", args.SwapID, "keyID", keyID, "bridge", args.Identifier, "swaptype", args.SwapType.String()) + time.Sleep(retryGetSignStatusInterval) + } + if i == retryGetSignStatusCount || len(rsv) == 0 { + return nil, errors.New("get sign status failed") + } + + rsv, err = b.adjustRsvOrders(rsv, msgHash, cfgFromPublicKey) + if err != nil { + return nil, err + } + + log.Trace(b.ChainConfig.BlockChain+" DcrmSignTransaction get rsv success", "keyID", keyID, "rsv", rsv) + return rsv, nil +} + +func (b *Bridge) adjustRsvOrders(rsvs, msgHashes []string, fromPublicKey string) (newRsvs []string, err error) { + if len(rsvs) <= 1 { + return rsvs, nil + } + fromPubkeyData, err := b.GetCompressedPublicKey(fromPublicKey, false) + matchedRsvMap := make(map[string]struct{}) + var cPkData []byte + for _, msgHash := range msgHashes { + matched := false + for _, rsv := range rsvs { + if _, exist := matchedRsvMap[rsv]; exist { + continue + } + cPkData, err = b.getPkDataFromSig(rsv, msgHash, true) + if err == nil && bytes.Equal(cPkData, fromPubkeyData) { + matchedRsvMap[rsv] = struct{}{} + newRsvs = append(newRsvs, rsv) + matched = true + break + } + } + if !matched { + return nil, fmt.Errorf("msgHash %v hash no matched rsv", msgHash) + } + } + return newRsvs, err +} + +// SignTransaction sign tx with pairID +func (b *Bridge) SignTransaction(rawTx interface{}, pairID string) (signedTx interface{}, txHash string, err error) { + privKey := b.GetTokenConfig(pairID).GetDcrmAddressPrivateKey() + return b.SignTransactionWithPrivateKey(rawTx, privKey) +} + +// SignTransactionWithWIF sign tx with WIF +func (b *Bridge) SignTransactionWithWIF(rawTx interface{}, wif string) (signedTx interface{}, txHash string, err error) { + pkwif, err := DecodeWIF(wif) + if err != nil { + return nil, "", err + } + return b.SignTransactionWithPrivateKey(rawTx, pkwif.PrivKey.ToECDSA()) +} + +// SignTransactionWithPrivateKey sign tx with ECDSA private key +func (b *Bridge) SignTransactionWithPrivateKey(rawTx interface{}, privKey *ecdsa.PrivateKey) (signTx interface{}, txHash string, err error) { + authoredTx, ok := rawTx.(*txauthor.AuthoredTx) + if !ok { + return nil, "", tokens.ErrWrongRawTx + } + + var ( + msgHashes []string + rsvs []string + sigScripts [][]byte + hasP2shInput bool + ) + + for i, preScript := range authoredTx.PrevScripts { + sigScript := preScript + if b.IsPayToScriptHash(preScript) { + sigScript, err = b.getRedeemScriptByOutputScrpit(preScript) + if err != nil { + return nil, "", err + } + hasP2shInput = true + } + + sigHash, err := b.CalcSignatureHash(sigScript, authoredTx.Tx, i) + if err != nil { + return nil, "", err + } + msgHash := hex.EncodeToString(sigHash) + msgHashes = append(msgHashes, msgHash) + sigScripts = append(sigScripts, sigScript) + } + if !hasP2shInput { + sigScripts = nil + } + + for _, msgHash := range msgHashes { + rsv, errf := b.SignWithECDSA(privKey, common.FromHex(msgHash)) + if errf != nil { + return nil, "", errf + } + rsvs = append(rsvs, rsv) + } + + cPkData := b.GetPublicKeyFromECDSA(privKey, true) + return b.MakeSignedTransaction(authoredTx, msgHashes, rsvs, sigScripts, cPkData) +} diff --git a/tokens/zol/verifyp2shtx.go b/tokens/zol/verifyp2shtx.go new file mode 100644 index 00000000..480b8a60 --- /dev/null +++ b/tokens/zol/verifyp2shtx.go @@ -0,0 +1,65 @@ +package zol + +import ( + "github.com/anyswap/CrossChain-Bridge/common" + "github.com/anyswap/CrossChain-Bridge/log" + "github.com/anyswap/CrossChain-Bridge/tokens" +) + +// VerifyP2shTransaction verify p2sh tx +func (b *Bridge) VerifyP2shTransaction(pairID, txHash, bindAddress string, allowUnstable bool) (*tokens.TxSwapInfo, error) { + if !b.IsSrc { + return nil, tokens.ErrBridgeDestinationNotSupported + } + return b.verifyP2shSwapinTx(pairID, txHash, bindAddress, allowUnstable) +} + +func (b *Bridge) verifyP2shSwapinTx(pairID, txHash, bindAddress string, allowUnstable bool) (*tokens.TxSwapInfo, error) { + tokenCfg := b.GetTokenConfig(pairID) + if tokenCfg == nil { + return nil, tokens.ErrUnknownPairID + } + swapInfo := &tokens.TxSwapInfo{} + swapInfo.PairID = pairID // PairID + swapInfo.Hash = txHash // Hash + swapInfo.Bind = bindAddress // Bind + p2shAddress, _, err := b.GetP2shAddress(bindAddress) + if err != nil { + return swapInfo, tokens.ErrWrongP2shBindAddress + } + if !allowUnstable && !b.checkStable(txHash) { + return swapInfo, tokens.ErrTxNotStable + } + tx, err := b.GetTransactionByHash(txHash) + if err != nil { + log.Debug("[verifyP2sh] "+b.ChainConfig.BlockChain+" Bridge::GetTransaction fail", "tx", txHash, "err", err) + return swapInfo, tokens.ErrTxNotFound + } + txStatus := tx.Status + if txStatus.BlockHeight != nil { + swapInfo.Height = *txStatus.BlockHeight // Height + } else if *tx.Locktime != 0 { + // tx with locktime should be on chain, prvent DDOS attack + return swapInfo, tokens.ErrTxNotStable + } + if txStatus.BlockTime != nil { + swapInfo.Timestamp = *txStatus.BlockTime // Timestamp + } + value, _, rightReceiver := b.GetReceivedValue(tx.Vout, p2shAddress, p2shType) + if !rightReceiver { + return swapInfo, tokens.ErrTxWithWrongReceiver + } + swapInfo.To = p2shAddress // To + swapInfo.Value = common.BigFromUint64(value) // Value + swapInfo.From = getTxFrom(tx.Vin, p2shAddress) // From + + err = b.checkSwapinInfo(swapInfo) + if err != nil { + return swapInfo, err + } + + if !allowUnstable { + log.Debug("verify p2sh swapin pass", "pairID", swapInfo.PairID, "from", swapInfo.From, "to", swapInfo.To, "bind", swapInfo.Bind, "value", swapInfo.Value, "txid", swapInfo.Hash, "height", swapInfo.Height, "timestamp", swapInfo.Timestamp) + } + return swapInfo, nil +} diff --git a/tokens/zol/verifytx.go b/tokens/zol/verifytx.go new file mode 100644 index 00000000..c874eba0 --- /dev/null +++ b/tokens/zol/verifytx.go @@ -0,0 +1,219 @@ +package zol + +import ( + "encoding/hex" + "regexp" + "strings" + + "github.com/anyswap/CrossChain-Bridge/common" + "github.com/anyswap/CrossChain-Bridge/log" + "github.com/anyswap/CrossChain-Bridge/tokens" + "github.com/anyswap/CrossChain-Bridge/tokens/btc/electrs" + "github.com/btcsuite/btcwallet/wallet/txauthor" +) + +var ( + regexMemo = regexp.MustCompile(`^OP_RETURN OP_PUSHBYTES_\d* `) +) + +// GetTransaction impl +func (b *Bridge) GetTransaction(txHash string) (interface{}, error) { + return b.GetTransactionByHash(txHash) +} + +// GetTransactionStatus impl +func (b *Bridge) GetTransactionStatus(txHash string) *tokens.TxStatus { + txStatus := &tokens.TxStatus{} + electStatus, err := b.GetElectTransactionStatus(txHash) + if err != nil { + log.Trace(b.ChainConfig.BlockChain+" Bridge::GetElectTransactionStatus fail", "tx", txHash, "err", err) + return txStatus + } + if !*electStatus.Confirmed { + return txStatus + } + if electStatus.BlockHash != nil { + txStatus.BlockHash = *electStatus.BlockHash + } + if electStatus.BlockTime != nil { + txStatus.BlockTime = *electStatus.BlockTime + } + if electStatus.BlockHeight != nil { + txStatus.BlockHeight = *electStatus.BlockHeight + latest, err := b.GetLatestBlockNumber() + if err != nil { + log.Debug(b.ChainConfig.BlockChain+" Bridge::GetLatestBlockNumber fail", "err", err) + return txStatus + } + if latest > txStatus.BlockHeight { + txStatus.Confirmations = latest - txStatus.BlockHeight + } + } + return txStatus +} + +// VerifyMsgHash verify msg hash +func (b *Bridge) VerifyMsgHash(rawTx interface{}, msgHash []string) (err error) { + authoredTx, ok := rawTx.(*txauthor.AuthoredTx) + if !ok { + return tokens.ErrWrongRawTx + } + for i, preScript := range authoredTx.PrevScripts { + sigScript := preScript + if b.IsPayToScriptHash(sigScript) { + sigScript, err = b.getRedeemScriptByOutputScrpit(preScript) + if err != nil { + return err + } + } + sigHash, err := b.CalcSignatureHash(sigScript, authoredTx.Tx, i) + if err != nil { + return err + } + if hex.EncodeToString(sigHash) != msgHash[i] { + log.Trace("message hash mismatch", "index", i, "want", msgHash[i], "have", hex.EncodeToString(sigHash)) + return tokens.ErrMsgHashMismatch + } + } + return nil +} + +// VerifyTransaction impl +func (b *Bridge) VerifyTransaction(pairID, txHash string, allowUnstable bool) (*tokens.TxSwapInfo, error) { + if !b.IsSrc { + return nil, tokens.ErrBridgeDestinationNotSupported + } + return b.verifySwapinTx(pairID, txHash, allowUnstable) +} + +func (b *Bridge) verifySwapinTx(pairID, txHash string, allowUnstable bool) (*tokens.TxSwapInfo, error) { + tokenCfg := b.GetTokenConfig(pairID) + if tokenCfg == nil { + return nil, tokens.ErrUnknownPairID + } + swapInfo := &tokens.TxSwapInfo{} + swapInfo.PairID = pairID // PairID + swapInfo.Hash = txHash // Hash + if !allowUnstable && !b.checkStable(txHash) { + return swapInfo, tokens.ErrTxNotStable + } + tx, err := b.GetTransactionByHash(txHash) + if err != nil { + log.Debug("[verifySwapin] "+b.ChainConfig.BlockChain+" Bridge::GetTransaction fail", "tx", txHash, "err", err) + return swapInfo, tokens.ErrTxNotFound + } + txStatus := tx.Status + if txStatus.BlockHeight != nil { + swapInfo.Height = *txStatus.BlockHeight // Height + } else if *tx.Locktime != 0 { + // tx with locktime should be on chain, prvent DDOS attack + return swapInfo, tokens.ErrTxNotStable + } + if txStatus.BlockTime != nil { + swapInfo.Timestamp = *txStatus.BlockTime // Timestamp + } + depositAddress := tokenCfg.DepositAddress + value, memoScript, rightReceiver := b.GetReceivedValue(tx.Vout, depositAddress, p2pkhType) + if !rightReceiver { + return swapInfo, tokens.ErrTxWithWrongReceiver + } + bindAddress, bindOk := GetBindAddressFromMemoScipt(memoScript) + + swapInfo.To = depositAddress // To + swapInfo.Value = common.BigFromUint64(value) // Value + swapInfo.Bind = bindAddress // Bind + swapInfo.From = getTxFrom(tx.Vin, depositAddress) // From + + err = b.checkSwapinInfo(swapInfo) + if err != nil { + return swapInfo, err + } + if !bindOk { + log.Debug("wrong memo", "memo", memoScript) + return swapInfo, tokens.ErrTxWithWrongMemo + } + + if !allowUnstable { + log.Debug("verify swapin pass", "pairID", swapInfo.PairID, "from", swapInfo.From, "to", swapInfo.To, "bind", swapInfo.Bind, "value", swapInfo.Value, "txid", swapInfo.Hash, "height", swapInfo.Height, "timestamp", swapInfo.Timestamp) + } + return swapInfo, nil +} + +func (b *Bridge) checkSwapinInfo(swapInfo *tokens.TxSwapInfo) error { + if swapInfo.From == swapInfo.To { + return tokens.ErrTxWithWrongSender + } + if !tokens.CheckSwapValue(swapInfo.PairID, swapInfo.Value, b.IsSrc) { + return tokens.ErrTxWithWrongValue + } + if !tokens.DstBridge.IsValidAddress(swapInfo.Bind) { + log.Debug("wrong bind address in swapin", "bind", swapInfo.Bind) + return tokens.ErrTxWithWrongMemo + } + return nil +} + +func (b *Bridge) checkStable(txHash string) bool { + txStatus := b.GetTransactionStatus(txHash) + confirmations := *b.GetChainConfig().Confirmations + return txStatus.BlockHeight > 0 && txStatus.Confirmations >= confirmations +} + +// GetReceivedValue get received value +func (b *Bridge) GetReceivedValue(vout []*electrs.ElectTxOut, receiver, pubkeyType string) (value uint64, memoScript string, rightReceiver bool) { + for _, output := range vout { + switch *output.ScriptpubkeyType { + case opReturnType: + memoScript = *output.ScriptpubkeyAsm + continue + case pubkeyType: + if output.ScriptpubkeyAddress == nil || *output.ScriptpubkeyAddress != receiver { + continue + } + rightReceiver = true + value += *output.Value + } + } + return value, memoScript, rightReceiver +} + +// return priorityAddress if has it in Vin +// return the first address in Vin if has no priorityAddress +func getTxFrom(vin []*electrs.ElectTxin, priorityAddress string) string { + from := "" + for _, input := range vin { + if input != nil && + input.Prevout != nil && + input.Prevout.ScriptpubkeyAddress != nil { + if *input.Prevout.ScriptpubkeyAddress == priorityAddress { + return priorityAddress + } + if from == "" { + from = *input.Prevout.ScriptpubkeyAddress + } + } + } + return from +} + +// GetBindAddressFromMemoScipt get bind address +func GetBindAddressFromMemoScipt(memoScript string) (bind string, ok bool) { + parts := regexMemo.Split(memoScript, -1) + if len(parts) != 2 { + return "", false + } + memoHex := strings.TrimSpace(parts[1]) + memo := common.FromHex(memoHex) + memoStr := string(memo) + if memoStr == tokens.AggregateMemo { + return "", false + } + if len(memo) <= len(tokens.LockMemoPrefix) { + return "", false + } + if !strings.HasPrefix(memoStr, tokens.LockMemoPrefix) { + return "", false + } + bind = string(memo[len(tokens.LockMemoPrefix):]) + return bind, true +}