diff --git a/cmd/vipsatomicswap/go.mod b/cmd/vipsatomicswap/go.mod new file mode 100644 index 0000000..ffe668f --- /dev/null +++ b/cmd/vipsatomicswap/go.mod @@ -0,0 +1,11 @@ +module github.com/decred/atomicswap/cmd/vipsatomicswap + +require ( + github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect + github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect + github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect + github.com/vipstar-dev/vipsd v0.0.0-20190307102440-2f4bb8391b83 + github.com/vipstar-dev/vipsutil v0.0.0-20190228101913-ca1b1c32c299 + github.com/vipstar-dev/vipswallet v0.0.0-20190307014835-1f6818b7fc48 + golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 +) diff --git a/cmd/vipsatomicswap/go.sum b/cmd/vipsatomicswap/go.sum new file mode 100644 index 0000000..806a77b --- /dev/null +++ b/cmd/vipsatomicswap/go.sum @@ -0,0 +1,15 @@ +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/vipstar-dev/vipsd v0.0.0-20190307102440-2f4bb8391b83 h1:sQls+tGEAozfmCnuyHZApGzxoQRK+/nLDNks1xipa1k= +github.com/vipstar-dev/vipsd v0.0.0-20190307102440-2f4bb8391b83/go.mod h1:G/+/yMcLBHoCoZc+4i4OvnUWuu1kWLPyTwJcqB3CWz0= +github.com/vipstar-dev/vipsutil v0.0.0-20190228101913-ca1b1c32c299 h1:U/oZN1CsHPFN74L0r90EaNLznh5S3POjYswDMYCeffI= +github.com/vipstar-dev/vipsutil v0.0.0-20190228101913-ca1b1c32c299/go.mod h1:n5E6s5Z4/mDiHa+F/0J8S+iYXCutaFeTGocRyItBL/w= +github.com/vipstar-dev/vipswallet v0.0.0-20190307014835-1f6818b7fc48 h1:wkaUmeYhkWO//1sePXy3KvonVZkbMGOeLWZMl36sets= +github.com/vipstar-dev/vipswallet v0.0.0-20190307014835-1f6818b7fc48/go.mod h1:nsX3t1w+V9BVYUKt0jtp75741aWLZX/LsmB07RP9UVQ= +golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 h1:jsG6UpNLt9iAsb0S2AGW28DveNzzgmbXR+ENoPjUeIU= +golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= \ No newline at end of file diff --git a/cmd/vipsatomicswap/main.go b/cmd/vipsatomicswap/main.go new file mode 100644 index 0000000..42746d8 --- /dev/null +++ b/cmd/vipsatomicswap/main.go @@ -0,0 +1,1147 @@ +// Copyright (c) 2017 The Decred developers +// Copyright (c) 2019 The VIPSTARCOIN developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package main + +import ( + "bufio" + "bytes" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "flag" + "fmt" + "net" + "os" + "strconv" + "strings" + "time" + + "github.com/vipstar-dev/vipsd/chaincfg" + "github.com/vipstar-dev/vipsd/chaincfg/chainhash" + rpc "github.com/vipstar-dev/vipsd/rpcclient" + "github.com/vipstar-dev/vipsd/txscript" + "github.com/vipstar-dev/vipsd/wire" + "github.com/vipstar-dev/vipsutil" + "github.com/vipstar-dev/vipswallet/wallet/txrules" + "golang.org/x/crypto/ripemd160" +) + +const verify = true + +const secretSize = 32 + +const txVersion = 2 + +var ( + chainParams = &chaincfg.MainNetParams +) + +var ( + flagset = flag.NewFlagSet("", flag.ExitOnError) + connectFlag = flagset.String("s", "localhost", "host[:port] of VIPSTARCOIN Core wallet RPC server") + rpcuserFlag = flagset.String("rpcuser", "", "username for wallet RPC authentication") + rpcpassFlag = flagset.String("rpcpass", "", "password for wallet RPC authentication") + testnetFlag = flagset.Bool("testnet", false, "use testnet network") +) + +// There are two directions that the atomic swap can be performed, as the +// initiator can be on either chain. This tool only deals with creating the +// VIPSTARCOIN transactions for these swaps. A second tool should be used for the +// transaction on the other chain. Any chain can be used so long as it supports +// OP_SHA256 and OP_CHECKLOCKTIMEVERIFY. +// +// Example scenerios using vipstarcoin as the second chain: +// +// Scenerio 1: +// cp1 initiates (dcr) +// cp2 participates with cp1 H(S) (vips) +// cp1 redeems vips revealing S +// - must verify H(S) in contract is hash of known secret +// cp2 redeems dcr with S +// +// Scenerio 2: +// cp1 initiates (vips) +// cp2 participates with cp1 H(S) (dcr) +// cp1 redeems dcr revealing S +// - must verify H(S) in contract is hash of known secret +// cp2 redeems vips with S + +func init() { + flagset.Usage = func() { + fmt.Println("Usage: vipsatomicswap [flags] cmd [cmd args]") + fmt.Println() + fmt.Println("Commands:") + fmt.Println(" initiate ") + fmt.Println(" participate ") + fmt.Println(" redeem ") + fmt.Println(" refund ") + fmt.Println(" extractsecret ") + fmt.Println(" auditcontract ") + fmt.Println() + fmt.Println("Flags:") + flagset.PrintDefaults() + } +} + +type command interface { + runCommand(*rpc.Client) error +} + +// offline commands don't require wallet RPC. +type offlineCommand interface { + command + runOfflineCommand() error +} + +type initiateCmd struct { + cp2Addr *vipsutil.AddressPubKeyHash + amount vipsutil.Amount +} + +type participateCmd struct { + cp1Addr *vipsutil.AddressPubKeyHash + amount vipsutil.Amount + secretHash []byte +} + +type redeemCmd struct { + contract []byte + contractTx *wire.MsgTx + secret []byte +} + +type refundCmd struct { + contract []byte + contractTx *wire.MsgTx +} + +type extractSecretCmd struct { + redemptionTx *wire.MsgTx + secretHash []byte +} + +type auditContractCmd struct { + contract []byte + contractTx *wire.MsgTx +} + +func main() { + err, showUsage := run() + if err != nil { + fmt.Fprintln(os.Stderr, err) + } + if showUsage { + flagset.Usage() + } + if err != nil || showUsage { + os.Exit(1) + } +} + +func checkCmdArgLength(args []string, required int) (nArgs int) { + if len(args) < required { + return 0 + } + for i, arg := range args[:required] { + if len(arg) != 1 && strings.HasPrefix(arg, "-") { + return i + } + } + return required +} + +func run() (err error, showUsage bool) { + flagset.Parse(os.Args[1:]) + args := flagset.Args() + if len(args) == 0 { + return nil, true + } + cmdArgs := 0 + switch args[0] { + case "initiate": + cmdArgs = 2 + case "participate": + cmdArgs = 3 + case "redeem": + cmdArgs = 3 + case "refund": + cmdArgs = 2 + case "extractsecret": + cmdArgs = 2 + case "auditcontract": + cmdArgs = 2 + default: + return fmt.Errorf("unknown command %v", args[0]), true + } + nArgs := checkCmdArgLength(args[1:], cmdArgs) + flagset.Parse(args[1+nArgs:]) + if nArgs < cmdArgs { + return fmt.Errorf("%s: too few arguments", args[0]), true + } + if flagset.NArg() != 0 { + return fmt.Errorf("unexpected argument: %s", flagset.Arg(0)), true + } + + if *testnetFlag { + chainParams = &chaincfg.TestNet3Params + } + + var cmd command + switch args[0] { + case "initiate": + cp2Addr, err := vipsutil.DecodeAddress(args[1], chainParams) + if err != nil { + return fmt.Errorf("failed to decode participant address: %v", err), true + } + if !cp2Addr.IsForNet(chainParams) { + return fmt.Errorf("participant address is not "+ + "intended for use on %v", chainParams.Name), true + } + cp2AddrP2PKH, ok := cp2Addr.(*vipsutil.AddressPubKeyHash) + if !ok { + return errors.New("participant address is not P2PKH"), true + } + + amountF64, err := strconv.ParseFloat(args[2], 64) + if err != nil { + return fmt.Errorf("failed to decode amount: %v", err), true + } + amount, err := vipsutil.NewAmount(amountF64) + if err != nil { + return err, true + } + + cmd = &initiateCmd{cp2Addr: cp2AddrP2PKH, amount: amount} + + case "participate": + cp1Addr, err := vipsutil.DecodeAddress(args[1], chainParams) + if err != nil { + return fmt.Errorf("failed to decode initiator address: %v", err), true + } + if !cp1Addr.IsForNet(chainParams) { + return fmt.Errorf("initiator address is not "+ + "intended for use on %v", chainParams.Name), true + } + cp1AddrP2PKH, ok := cp1Addr.(*vipsutil.AddressPubKeyHash) + if !ok { + return errors.New("initiator address is not P2PKH"), true + } + + amountF64, err := strconv.ParseFloat(args[2], 64) + if err != nil { + return fmt.Errorf("failed to decode amount: %v", err), true + } + amount, err := vipsutil.NewAmount(amountF64) + if err != nil { + return err, true + } + + secretHash, err := hex.DecodeString(args[3]) + if err != nil { + return errors.New("secret hash must be hex encoded"), true + } + if len(secretHash) != sha256.Size { + return errors.New("secret hash has wrong size"), true + } + + cmd = &participateCmd{cp1Addr: cp1AddrP2PKH, amount: amount, secretHash: secretHash} + + case "redeem": + contract, err := hex.DecodeString(args[1]) + if err != nil { + return fmt.Errorf("failed to decode contract: %v", err), true + } + + contractTxBytes, err := hex.DecodeString(args[2]) + if err != nil { + return fmt.Errorf("failed to decode contract transaction: %v", err), true + } + var contractTx wire.MsgTx + err = contractTx.Deserialize(bytes.NewReader(contractTxBytes)) + if err != nil { + return fmt.Errorf("failed to decode contract transaction: %v", err), true + } + + secret, err := hex.DecodeString(args[3]) + if err != nil { + return fmt.Errorf("failed to decode secret: %v", err), true + } + + cmd = &redeemCmd{contract: contract, contractTx: &contractTx, secret: secret} + + case "refund": + contract, err := hex.DecodeString(args[1]) + if err != nil { + return fmt.Errorf("failed to decode contract: %v", err), true + } + + contractTxBytes, err := hex.DecodeString(args[2]) + if err != nil { + return fmt.Errorf("failed to decode contract transaction: %v", err), true + } + var contractTx wire.MsgTx + err = contractTx.Deserialize(bytes.NewReader(contractTxBytes)) + if err != nil { + return fmt.Errorf("failed to decode contract transaction: %v", err), true + } + + cmd = &refundCmd{contract: contract, contractTx: &contractTx} + + case "extractsecret": + redemptionTxBytes, err := hex.DecodeString(args[1]) + if err != nil { + return fmt.Errorf("failed to decode redemption transaction: %v", err), true + } + var redemptionTx wire.MsgTx + err = redemptionTx.Deserialize(bytes.NewReader(redemptionTxBytes)) + if err != nil { + return fmt.Errorf("failed to decode redemption transaction: %v", err), true + } + + secretHash, err := hex.DecodeString(args[2]) + if err != nil { + return errors.New("secret hash must be hex encoded"), true + } + if len(secretHash) != sha256.Size { + return errors.New("secret hash has wrong size"), true + } + + cmd = &extractSecretCmd{redemptionTx: &redemptionTx, secretHash: secretHash} + + case "auditcontract": + contract, err := hex.DecodeString(args[1]) + if err != nil { + return fmt.Errorf("failed to decode contract: %v", err), true + } + + contractTxBytes, err := hex.DecodeString(args[2]) + if err != nil { + return fmt.Errorf("failed to decode contract transaction: %v", err), true + } + var contractTx wire.MsgTx + err = contractTx.Deserialize(bytes.NewReader(contractTxBytes)) + if err != nil { + return fmt.Errorf("failed to decode contract transaction: %v", err), true + } + + cmd = &auditContractCmd{contract: contract, contractTx: &contractTx} + } + + // Offline commands don't need to talk to the wallet. + if cmd, ok := cmd.(offlineCommand); ok { + return cmd.runOfflineCommand(), false + } + + connect, err := normalizeAddress(*connectFlag, walletPort(chainParams)) + if err != nil { + return fmt.Errorf("wallet server address: %v", err), true + } + + connConfig := &rpc.ConnConfig{ + Host: connect, + User: *rpcuserFlag, + Pass: *rpcpassFlag, + DisableTLS: true, + HTTPPostMode: true, + } + client, err := rpc.New(connConfig, nil) + if err != nil { + return fmt.Errorf("rpc connect: %v", err), false + } + defer func() { + client.Shutdown() + client.WaitForShutdown() + }() + + err = cmd.runCommand(client) + return err, false +} + +func normalizeAddress(addr string, defaultPort string) (hostport string, err error) { + host, port, origErr := net.SplitHostPort(addr) + if origErr == nil { + return net.JoinHostPort(host, port), nil + } + addr = net.JoinHostPort(addr, defaultPort) + _, _, err = net.SplitHostPort(addr) + if err != nil { + return "", origErr + } + return addr, nil +} + +func walletPort(params *chaincfg.Params) string { + switch params { + case &chaincfg.MainNetParams: + return "31916" + case &chaincfg.TestNet3Params: + return "32916" + default: + return "" + } +} + +// createSig creates and returns the serialized raw signature and compressed +// pubkey for a transaction input signature. Due to limitations of the VIPSTARCOIN +// Core RPC API, this requires dumping a private key and signing in the client, +// rather than letting the wallet sign. +func createSig(tx *wire.MsgTx, idx int, pkScript []byte, addr vipsutil.Address, + c *rpc.Client) (sig, pubkey []byte, err error) { + + wif, err := c.DumpPrivKey(addr) + if err != nil { + return nil, nil, err + } + sig, err = txscript.RawTxInSignature(tx, idx, pkScript, txscript.SigHashAll, wif.PrivKey) + if err != nil { + return nil, nil, err + } + return sig, wif.PrivKey.PubKey().SerializeCompressed(), nil +} + +// fundRawTransaction calls the fundrawtransaction JSON-RPC method. It is +// implemented manually as client support is currently missing from the +// vipsd/rpcclient package. +func fundRawTransaction(c *rpc.Client, tx *wire.MsgTx, feePerKb vipsutil.Amount) (fundedTx *wire.MsgTx, fee vipsutil.Amount, err error) { + var buf bytes.Buffer + buf.Grow(tx.SerializeSize()) + tx.Serialize(&buf) + param0, err := json.Marshal(hex.EncodeToString(buf.Bytes())) + if err != nil { + return nil, 0, err + } + param1, err := json.Marshal(struct { + FeeRate float64 `json:"feeRate"` + }{ + FeeRate: feePerKb.ToBTC(), + }) + if err != nil { + return nil, 0, err + } + params := []json.RawMessage{param0, param1} + rawResp, err := c.RawRequest("fundrawtransaction", params) + if err != nil { + return nil, 0, err + } + var resp struct { + Hex string `json:"hex"` + Fee float64 `json:"fee"` + ChangePos float64 `json:"changepos"` + } + err = json.Unmarshal(rawResp, &resp) + if err != nil { + return nil, 0, err + } + fundedTxBytes, err := hex.DecodeString(resp.Hex) + if err != nil { + return nil, 0, err + } + fundedTx = &wire.MsgTx{} + err = fundedTx.Deserialize(bytes.NewReader(fundedTxBytes)) + if err != nil { + return nil, 0, err + } + feeAmount, err := vipsutil.NewAmount(resp.Fee) + if err != nil { + return nil, 0, err + } + return fundedTx, feeAmount, nil +} + +// getFeePerKb queries the wallet for the transaction relay fee/kB to use and +// the minimum mempool relay fee. It first tries to get the user-set fee in the +// wallet. If unset, it attempts to find an estimate using estimatefee 6. If +// both of these fail, it falls back to mempool relay fee policy. +func getFeePerKb(c *rpc.Client) (useFee, relayFee vipsutil.Amount, err error) { + var netInfoResp struct { + RelayFee float64 `json:"relayfee"` + } + var walletInfoResp struct { + PayTxFee float64 `json:"paytxfee"` + } + var estimateResp struct { + FeeRate float64 `json:"feerate"` + } + + netInfoRawResp, err := c.RawRequest("getnetworkinfo", nil) + if err == nil { + err = json.Unmarshal(netInfoRawResp, &netInfoResp) + if err != nil { + return 0, 0, err + } + } + walletInfoRawResp, err := c.RawRequest("getwalletinfo", nil) + if err == nil { + err = json.Unmarshal(walletInfoRawResp, &walletInfoResp) + if err != nil { + return 0, 0, err + } + } + + relayFee, err = vipsutil.NewAmount(netInfoResp.RelayFee) + if err != nil { + return 0, 0, err + } + payTxFee, err := vipsutil.NewAmount(walletInfoResp.PayTxFee) + if err != nil { + return 0, 0, err + } + + // Use user-set wallet fee when set and not lower than the network relay + // fee. + if payTxFee != 0 { + maxFee := payTxFee + if relayFee > maxFee { + maxFee = relayFee + } + return maxFee, relayFee, nil + } + + params := []json.RawMessage{[]byte("6")} + estimateRawResp, err := c.RawRequest("estimatesmartfee", params) + if err != nil { + return 0, 0, err + } + + err = json.Unmarshal(estimateRawResp, &estimateResp) + if err == nil && estimateResp.FeeRate > 0 { + useFee, err = vipsutil.NewAmount(estimateResp.FeeRate) + if relayFee > useFee { + useFee = relayFee + } + return useFee, relayFee, err + } + + fmt.Println("warning: falling back to mempool relay fee policy") + return relayFee, relayFee, nil +} + +// getRawChangeAddress calls the getrawchangeaddress JSON-RPC method. It is +// implemented manually as the rpcclient implementation always passes the +// account parameter which was removed in VIPSTARCOIN Core 0.15. +func getRawChangeAddress(c *rpc.Client) (vipsutil.Address, error) { + params := []json.RawMessage{[]byte(`"legacy"`)} + rawResp, err := c.RawRequest("getrawchangeaddress", params) + if err != nil { + return nil, err + } + var addrStr string + err = json.Unmarshal(rawResp, &addrStr) + if err != nil { + return nil, err + } + addr, err := vipsutil.DecodeAddress(addrStr, chainParams) + if err != nil { + return nil, err + } + if !addr.IsForNet(chainParams) { + return nil, fmt.Errorf("address %v is not intended for use on %v", + addrStr, chainParams.Name) + } + if _, ok := addr.(*vipsutil.AddressPubKeyHash); !ok { + return nil, fmt.Errorf("getrawchangeaddress: address %v is not P2PKH", + addr) + } + return addr, nil +} + +func promptPublishTx(c *rpc.Client, tx *wire.MsgTx, name string) error { + reader := bufio.NewReader(os.Stdin) + for { + fmt.Printf("Publish %s transaction? [y/N] ", name) + answer, err := reader.ReadString('\n') + if err != nil { + return err + } + answer = strings.TrimSpace(strings.ToLower(answer)) + + switch answer { + case "y", "yes": + case "n", "no", "": + return nil + default: + fmt.Println("please answer y or n") + continue + } + + txHash, err := c.SendRawTransaction(tx, false) + if err != nil { + return fmt.Errorf("sendrawtransaction: %v", err) + } + fmt.Printf("Published %s transaction (%v)\n", name, txHash) + return nil + } +} + +// contractArgs specifies the common parameters used to create the initiator's +// and participant's contract. +type contractArgs struct { + them *vipsutil.AddressPubKeyHash + amount vipsutil.Amount + locktime int64 + secretHash []byte +} + +// builtContract houses the details regarding a contract and the contract +// payment transaction, as well as the transaction to perform a refund. +type builtContract struct { + contract []byte + contractP2SH vipsutil.Address + contractTxHash *chainhash.Hash + contractTx *wire.MsgTx + contractFee vipsutil.Amount + refundTx *wire.MsgTx + refundFee vipsutil.Amount +} + +// buildContract creates a contract for the parameters specified in args, using +// wallet RPC to generate an internal address to redeem the refund and to sign +// the payment to the contract transaction. +func buildContract(c *rpc.Client, args *contractArgs) (*builtContract, error) { + refundAddr, err := getRawChangeAddress(c) + if err != nil { + return nil, fmt.Errorf("getrawchangeaddress: %v", err) + } + refundAddrH, ok := refundAddr.(interface { + Hash160() *[ripemd160.Size]byte + }) + if !ok { + return nil, errors.New("unable to create hash160 from change address") + } + + contract, err := atomicSwapContract(refundAddrH.Hash160(), args.them.Hash160(), + args.locktime, args.secretHash) + if err != nil { + return nil, err + } + contractP2SH, err := vipsutil.NewAddressScriptHash(contract, chainParams) + if err != nil { + return nil, err + } + contractP2SHPkScript, err := txscript.PayToAddrScript(contractP2SH) + if err != nil { + return nil, err + } + + feePerKb, minFeePerKb, err := getFeePerKb(c) + if err != nil { + return nil, err + } + + unsignedContract := wire.NewMsgTx(txVersion) + unsignedContract.AddTxOut(wire.NewTxOut(int64(args.amount), contractP2SHPkScript)) + unsignedContract, contractFee, err := fundRawTransaction(c, unsignedContract, feePerKb) + if err != nil { + return nil, fmt.Errorf("fundrawtransaction: %v", err) + } + contractTx, complete, err := c.SignRawTransaction(unsignedContract) + if err != nil { + return nil, fmt.Errorf("signrawtransaction: %v", err) + } + if !complete { + return nil, errors.New("signrawtransaction: failed to completely sign contract transaction") + } + + contractTxHash := contractTx.TxHash() + + refundTx, refundFee, err := buildRefund(c, contract, contractTx, feePerKb, minFeePerKb) + if err != nil { + return nil, err + } + + return &builtContract{ + contract, + contractP2SH, + &contractTxHash, + contractTx, + contractFee, + refundTx, + refundFee, + }, nil +} + +func buildRefund(c *rpc.Client, contract []byte, contractTx *wire.MsgTx, feePerKb, minFeePerKb vipsutil.Amount) ( + refundTx *wire.MsgTx, refundFee vipsutil.Amount, err error) { + + contractP2SH, err := vipsutil.NewAddressScriptHash(contract, chainParams) + if err != nil { + return nil, 0, err + } + contractP2SHPkScript, err := txscript.PayToAddrScript(contractP2SH) + if err != nil { + return nil, 0, err + } + + contractTxHash := contractTx.TxHash() + contractOutPoint := wire.OutPoint{Hash: contractTxHash, Index: ^uint32(0)} + for i, o := range contractTx.TxOut { + if bytes.Equal(o.PkScript, contractP2SHPkScript) { + contractOutPoint.Index = uint32(i) + break + } + } + if contractOutPoint.Index == ^uint32(0) { + return nil, 0, errors.New("contract tx does not contain a P2SH contract payment") + } + + refundAddress, err := getRawChangeAddress(c) + if err != nil { + return nil, 0, fmt.Errorf("getrawchangeaddress: %v", err) + } + refundOutScript, err := txscript.PayToAddrScript(refundAddress) + if err != nil { + return nil, 0, err + } + + pushes, err := txscript.ExtractAtomicSwapDataPushes(0, contract) + if err != nil { + // expected to only be called with good input + panic(err) + } + + refundAddr, err := vipsutil.NewAddressPubKeyHash(pushes.RefundHash160[:], chainParams) + if err != nil { + return nil, 0, err + } + + refundTx = wire.NewMsgTx(txVersion) + refundTx.LockTime = uint32(pushes.LockTime) + refundTx.AddTxOut(wire.NewTxOut(0, refundOutScript)) // amount set below + refundSize := estimateRefundSerializeSize(contract, refundTx.TxOut) + refundFee = txrules.FeeForSerializeSize(feePerKb, refundSize) + refundTx.TxOut[0].Value = contractTx.TxOut[contractOutPoint.Index].Value - int64(refundFee) + if txrules.IsDustOutput(refundTx.TxOut[0], minFeePerKb) { + return nil, 0, fmt.Errorf("refund output value of %v is dust", vipsutil.Amount(refundTx.TxOut[0].Value)) + } + + txIn := wire.NewTxIn(&contractOutPoint, nil, nil) + txIn.Sequence = 0 + refundTx.AddTxIn(txIn) + + refundSig, refundPubKey, err := createSig(refundTx, 0, contract, refundAddr, c) + if err != nil { + return nil, 0, err + } + refundSigScript, err := refundP2SHContract(contract, refundSig, refundPubKey) + if err != nil { + return nil, 0, err + } + refundTx.TxIn[0].SignatureScript = refundSigScript + + if verify { + e, err := txscript.NewEngine(contractTx.TxOut[contractOutPoint.Index].PkScript, + refundTx, 0, txscript.StandardVerifyFlags, txscript.NewSigCache(10), + txscript.NewTxSigHashes(refundTx), contractTx.TxOut[contractOutPoint.Index].Value) + if err != nil { + panic(err) + } + err = e.Execute() + if err != nil { + panic(err) + } + } + + return refundTx, refundFee, nil +} + +func sha256Hash(x []byte) []byte { + h := sha256.Sum256(x) + return h[:] +} + +func calcFeePerKb(absoluteFee vipsutil.Amount, serializeSize int) float64 { + return float64(absoluteFee) / float64(serializeSize) / 1e5 +} + +func (cmd *initiateCmd) runCommand(c *rpc.Client) error { + var secret [secretSize]byte + _, err := rand.Read(secret[:]) + if err != nil { + return err + } + secretHash := sha256Hash(secret[:]) + + // locktime after 500,000,000 (Tue Nov 5 00:53:20 1985 UTC) is interpreted + // as a unix time rather than a block height. + locktime := time.Now().Add(48 * time.Hour).Unix() + + b, err := buildContract(c, &contractArgs{ + them: cmd.cp2Addr, + amount: cmd.amount, + locktime: locktime, + secretHash: secretHash, + }) + if err != nil { + return err + } + + refundTxHash := b.refundTx.TxHash() + contractFeePerKb := calcFeePerKb(b.contractFee, b.contractTx.SerializeSize()) + refundFeePerKb := calcFeePerKb(b.refundFee, b.refundTx.SerializeSize()) + + fmt.Printf("Secret: %x\n", secret) + fmt.Printf("Secret hash: %x\n\n", secretHash) + fmt.Printf("Contract fee: %v (%0.8f VIPS/kB)\n", b.contractFee, contractFeePerKb) + fmt.Printf("Refund fee: %v (%0.8f VIPS/kB)\n\n", b.refundFee, refundFeePerKb) + fmt.Printf("Contract (%v):\n", b.contractP2SH) + fmt.Printf("%x\n\n", b.contract) + var contractBuf bytes.Buffer + contractBuf.Grow(b.contractTx.SerializeSize()) + b.contractTx.Serialize(&contractBuf) + fmt.Printf("Contract transaction (%v):\n", b.contractTxHash) + fmt.Printf("%x\n\n", contractBuf.Bytes()) + var refundBuf bytes.Buffer + refundBuf.Grow(b.refundTx.SerializeSize()) + b.refundTx.Serialize(&refundBuf) + fmt.Printf("Refund transaction (%v):\n", &refundTxHash) + fmt.Printf("%x\n\n", refundBuf.Bytes()) + + return promptPublishTx(c, b.contractTx, "contract") +} + +func (cmd *participateCmd) runCommand(c *rpc.Client) error { + // locktime after 500,000,000 (Tue Nov 5 00:53:20 1985 UTC) is interpreted + // as a unix time rather than a block height. + locktime := time.Now().Add(24 * time.Hour).Unix() + + b, err := buildContract(c, &contractArgs{ + them: cmd.cp1Addr, + amount: cmd.amount, + locktime: locktime, + secretHash: cmd.secretHash, + }) + if err != nil { + return err + } + + refundTxHash := b.refundTx.TxHash() + contractFeePerKb := calcFeePerKb(b.contractFee, b.contractTx.SerializeSize()) + refundFeePerKb := calcFeePerKb(b.refundFee, b.refundTx.SerializeSize()) + + fmt.Printf("Contract fee: %v (%0.8f VIPS/kB)\n", b.contractFee, contractFeePerKb) + fmt.Printf("Refund fee: %v (%0.8f VIPS/kB)\n\n", b.refundFee, refundFeePerKb) + fmt.Printf("Contract (%v):\n", b.contractP2SH) + fmt.Printf("%x\n\n", b.contract) + var contractBuf bytes.Buffer + contractBuf.Grow(b.contractTx.SerializeSize()) + b.contractTx.Serialize(&contractBuf) + fmt.Printf("Contract transaction (%v):\n", b.contractTxHash) + fmt.Printf("%x\n\n", contractBuf.Bytes()) + var refundBuf bytes.Buffer + refundBuf.Grow(b.refundTx.SerializeSize()) + b.refundTx.Serialize(&refundBuf) + fmt.Printf("Refund transaction (%v):\n", &refundTxHash) + fmt.Printf("%x\n\n", refundBuf.Bytes()) + + return promptPublishTx(c, b.contractTx, "contract") +} + +func (cmd *redeemCmd) runCommand(c *rpc.Client) error { + pushes, err := txscript.ExtractAtomicSwapDataPushes(0, cmd.contract) + if err != nil { + return err + } + if pushes == nil { + return errors.New("contract is not an atomic swap script recognized by this tool") + } + recipientAddr, err := vipsutil.NewAddressPubKeyHash(pushes.RecipientHash160[:], + chainParams) + if err != nil { + return err + } + contractHash := vipsutil.Hash160(cmd.contract) + contractOut := -1 + for i, out := range cmd.contractTx.TxOut { + sc, addrs, _, _ := txscript.ExtractPkScriptAddrs(out.PkScript, chainParams) + if sc == txscript.ScriptHashTy && + bytes.Equal(addrs[0].(*vipsutil.AddressScriptHash).Hash160()[:], contractHash) { + contractOut = i + break + } + } + if contractOut == -1 { + return errors.New("transaction does not contain a contract output") + } + + addr, err := getRawChangeAddress(c) + if err != nil { + return fmt.Errorf("getrawchangeaddres: %v", err) + } + outScript, err := txscript.PayToAddrScript(addr) + if err != nil { + return err + } + + contractTxHash := cmd.contractTx.TxHash() + contractOutPoint := wire.OutPoint{ + Hash: contractTxHash, + Index: uint32(contractOut), + } + + feePerKb, minFeePerKb, err := getFeePerKb(c) + if err != nil { + return err + } + + redeemTx := wire.NewMsgTx(txVersion) + redeemTx.LockTime = uint32(pushes.LockTime) + redeemTx.AddTxIn(wire.NewTxIn(&contractOutPoint, nil, nil)) + redeemTx.AddTxOut(wire.NewTxOut(0, outScript)) // amount set below + redeemSize := estimateRedeemSerializeSize(cmd.contract, redeemTx.TxOut) + fee := txrules.FeeForSerializeSize(feePerKb, redeemSize) + redeemTx.TxOut[0].Value = cmd.contractTx.TxOut[contractOut].Value - int64(fee) + if txrules.IsDustOutput(redeemTx.TxOut[0], minFeePerKb) { + return fmt.Errorf("redeem output value of %v is dust", vipsutil.Amount(redeemTx.TxOut[0].Value)) + } + + redeemSig, redeemPubKey, err := createSig(redeemTx, 0, cmd.contract, recipientAddr, c) + if err != nil { + return err + } + redeemSigScript, err := redeemP2SHContract(cmd.contract, redeemSig, redeemPubKey, cmd.secret) + if err != nil { + return err + } + redeemTx.TxIn[0].SignatureScript = redeemSigScript + + redeemTxHash := redeemTx.TxHash() + redeemFeePerKb := calcFeePerKb(fee, redeemTx.SerializeSize()) + + var buf bytes.Buffer + buf.Grow(redeemTx.SerializeSize()) + redeemTx.Serialize(&buf) + fmt.Printf("Redeem fee: %v (%0.8f VIPS/kB)\n\n", fee, redeemFeePerKb) + fmt.Printf("Redeem transaction (%v):\n", &redeemTxHash) + fmt.Printf("%x\n\n", buf.Bytes()) + + if verify { + e, err := txscript.NewEngine(cmd.contractTx.TxOut[contractOutPoint.Index].PkScript, + redeemTx, 0, txscript.StandardVerifyFlags, txscript.NewSigCache(10), + txscript.NewTxSigHashes(redeemTx), cmd.contractTx.TxOut[contractOut].Value) + if err != nil { + panic(err) + } + err = e.Execute() + if err != nil { + panic(err) + } + } + + return promptPublishTx(c, redeemTx, "redeem") +} + +func (cmd *refundCmd) runCommand(c *rpc.Client) error { + pushes, err := txscript.ExtractAtomicSwapDataPushes(0, cmd.contract) + if err != nil { + return err + } + if pushes == nil { + return errors.New("contract is not an atomic swap script recognized by this tool") + } + + feePerKb, minFeePerKb, err := getFeePerKb(c) + if err != nil { + return err + } + + refundTx, refundFee, err := buildRefund(c, cmd.contract, cmd.contractTx, feePerKb, minFeePerKb) + if err != nil { + return err + } + refundTxHash := refundTx.TxHash() + var buf bytes.Buffer + buf.Grow(refundTx.SerializeSize()) + refundTx.Serialize(&buf) + + refundFeePerKb := calcFeePerKb(refundFee, refundTx.SerializeSize()) + + fmt.Printf("Refund fee: %v (%0.8f VIPS/kB)\n\n", refundFee, refundFeePerKb) + fmt.Printf("Refund transaction (%v):\n", &refundTxHash) + fmt.Printf("%x\n\n", buf.Bytes()) + + return promptPublishTx(c, refundTx, "refund") +} + +func (cmd *extractSecretCmd) runCommand(c *rpc.Client) error { + return cmd.runOfflineCommand() +} + +func (cmd *extractSecretCmd) runOfflineCommand() error { + // Loop over all pushed data from all inputs, searching for one that hashes + // to the expected hash. By searching through all data pushes, we avoid any + // issues that could be caused by the initiator redeeming the participant's + // contract with some "nonstandard" or unrecognized transaction or script + // type. + for _, in := range cmd.redemptionTx.TxIn { + pushes, err := txscript.PushedData(in.SignatureScript) + if err != nil { + return err + } + for _, push := range pushes { + if bytes.Equal(sha256Hash(push), cmd.secretHash) { + fmt.Printf("Secret: %x\n", push) + return nil + } + } + } + return errors.New("transaction does not contain the secret") +} + +func (cmd *auditContractCmd) runCommand(c *rpc.Client) error { + return cmd.runOfflineCommand() +} + +func (cmd *auditContractCmd) runOfflineCommand() error { + contractHash160 := vipsutil.Hash160(cmd.contract) + contractOut := -1 + for i, out := range cmd.contractTx.TxOut { + sc, addrs, _, err := txscript.ExtractPkScriptAddrs(out.PkScript, chainParams) + if err != nil || sc != txscript.ScriptHashTy { + continue + } + if bytes.Equal(addrs[0].(*vipsutil.AddressScriptHash).Hash160()[:], contractHash160) { + contractOut = i + break + } + } + if contractOut == -1 { + return errors.New("transaction does not contain the contract output") + } + + pushes, err := txscript.ExtractAtomicSwapDataPushes(0, cmd.contract) + if err != nil { + return err + } + if pushes == nil { + return errors.New("contract is not an atomic swap script recognized by this tool") + } + if pushes.SecretSize != secretSize { + return fmt.Errorf("contract specifies strange secret size %v", pushes.SecretSize) + } + + contractAddr, err := vipsutil.NewAddressScriptHash(cmd.contract, chainParams) + if err != nil { + return err + } + recipientAddr, err := vipsutil.NewAddressPubKeyHash(pushes.RecipientHash160[:], + chainParams) + if err != nil { + return err + } + refundAddr, err := vipsutil.NewAddressPubKeyHash(pushes.RefundHash160[:], + chainParams) + if err != nil { + return err + } + + fmt.Printf("Contract address: %v\n", contractAddr) + fmt.Printf("Contract value: %v\n", vipsutil.Amount(cmd.contractTx.TxOut[contractOut].Value)) + fmt.Printf("Recipient address: %v\n", recipientAddr) + fmt.Printf("Author's refund address: %v\n\n", refundAddr) + + fmt.Printf("Secret hash: %x\n\n", pushes.SecretHash[:]) + + if pushes.LockTime >= int64(txscript.LockTimeThreshold) { + t := time.Unix(pushes.LockTime, 0) + fmt.Printf("Locktime: %v\n", t.UTC()) + reachedAt := time.Until(t).Truncate(time.Second) + if reachedAt > 0 { + fmt.Printf("Locktime reached in %v\n", reachedAt) + } else { + fmt.Printf("Contract refund time lock has expired\n") + } + } else { + fmt.Printf("Locktime: block %v\n", pushes.LockTime) + } + + return nil +} + +// atomicSwapContract returns an output script that may be redeemed by one of +// two signature scripts: +// +// 1 +// +// 0 +// +// The first signature script is the normal redemption path done by the other +// party and requires the initiator's secret. The second signature script is +// the refund path performed by us, but the refund can only be performed after +// locktime. +func atomicSwapContract(pkhMe, pkhThem *[ripemd160.Size]byte, locktime int64, secretHash []byte) ([]byte, error) { + b := txscript.NewScriptBuilder() + + b.AddOp(txscript.OP_IF) // Normal redeem path + { + // Require initiator's secret to be a known length that the redeeming + // party can audit. This is used to prevent fraud attacks between two + // currencies that have different maximum data sizes. + b.AddOp(txscript.OP_SIZE) + b.AddInt64(secretSize) + b.AddOp(txscript.OP_EQUALVERIFY) + + // Require initiator's secret to be known to redeem the output. + b.AddOp(txscript.OP_SHA256) + b.AddData(secretHash) + b.AddOp(txscript.OP_EQUALVERIFY) + + // Verify their signature is being used to redeem the output. This + // would normally end with OP_EQUALVERIFY OP_CHECKSIG but this has been + // moved outside of the branch to save a couple bytes. + b.AddOp(txscript.OP_DUP) + b.AddOp(txscript.OP_HASH160) + b.AddData(pkhThem[:]) + } + b.AddOp(txscript.OP_ELSE) // Refund path + { + // Verify locktime and drop it off the stack (which is not done by + // CLTV). + b.AddInt64(locktime) + b.AddOp(txscript.OP_CHECKLOCKTIMEVERIFY) + b.AddOp(txscript.OP_DROP) + + // Verify our signature is being used to redeem the output. This would + // normally end with OP_EQUALVERIFY OP_CHECKSIG but this has been moved + // outside of the branch to save a couple bytes. + b.AddOp(txscript.OP_DUP) + b.AddOp(txscript.OP_HASH160) + b.AddData(pkhMe[:]) + } + b.AddOp(txscript.OP_ENDIF) + + // Complete the signature check. + b.AddOp(txscript.OP_EQUALVERIFY) + b.AddOp(txscript.OP_CHECKSIG) + + return b.Script() +} + +// redeemP2SHContract returns the signature script to redeem a contract output +// using the redeemer's signature and the initiator's secret. This function +// assumes P2SH and appends the contract as the final data push. +func redeemP2SHContract(contract, sig, pubkey, secret []byte) ([]byte, error) { + b := txscript.NewScriptBuilder() + b.AddData(sig) + b.AddData(pubkey) + b.AddData(secret) + b.AddInt64(1) + b.AddData(contract) + return b.Script() +} + +// refundP2SHContract returns the signature script to refund a contract output +// using the contract author's signature after the locktime has been reached. +// This function assumes P2SH and appends the contract as the final data push. +func refundP2SHContract(contract, sig, pubkey []byte) ([]byte, error) { + b := txscript.NewScriptBuilder() + b.AddData(sig) + b.AddData(pubkey) + b.AddInt64(0) + b.AddData(contract) + return b.Script() +} diff --git a/cmd/vipsatomicswap/sizeest.go b/cmd/vipsatomicswap/sizeest.go new file mode 100644 index 0000000..fdea7b6 --- /dev/null +++ b/cmd/vipsatomicswap/sizeest.go @@ -0,0 +1,92 @@ +// Copyright (c) 2016 The btcsuite developers +// Copyright (c) 2016-2017 The Decred developers +// Copyright (c) 2019 The VIPSTARCOIN developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package main + +import ( + "github.com/vipstar-dev/vipsd/txscript" + "github.com/vipstar-dev/vipsd/wire" +) + +// Worst case script and input/output size estimates. +const ( + // redeemAtomicSwapSigScriptSize is the worst case (largest) serialize size + // of a transaction input script to redeem the atomic swap contract. This + // does not include final push for the contract itself. + // + // - OP_DATA_73 + // - 72 bytes DER signature + 1 byte sighash + // - OP_DATA_33 + // - 33 bytes serialized compressed pubkey + // - OP_DATA_32 + // - 32 bytes secret + // - OP_TRUE + redeemAtomicSwapSigScriptSize = 1 + 73 + 1 + 33 + 1 + 32 + 1 + + // refundAtomicSwapSigScriptSize is the worst case (largest) serialize size + // of a transaction input script that refunds a P2SH atomic swap output. + // This does not include final push for the contract itself. + // + // - OP_DATA_73 + // - 72 bytes DER signature + 1 byte sighash + // - OP_DATA_33 + // - 33 bytes serialized compressed pubkey + // - OP_FALSE + refundAtomicSwapSigScriptSize = 1 + 73 + 1 + 33 + 1 +) + +func sumOutputSerializeSizes(outputs []*wire.TxOut) (serializeSize int) { + for _, txOut := range outputs { + serializeSize += txOut.SerializeSize() + } + return serializeSize +} + +// inputSize returns the size of the transaction input needed to include a +// signature script with size sigScriptSize. It is calculated as: +// +// - 32 bytes previous tx +// - 4 bytes output index +// - Compact int encoding sigScriptSize +// - sigScriptSize bytes signature script +// - 4 bytes sequence +func inputSize(sigScriptSize int) int { + return 32 + 4 + wire.VarIntSerializeSize(uint64(sigScriptSize)) + sigScriptSize + 4 +} + +// estimateRedeemSerializeSize returns a worst case serialize size estimates for +// a transaction that redeems an atomic swap P2SH output. +func estimateRedeemSerializeSize(contract []byte, txOuts []*wire.TxOut) int { + contractPush, err := txscript.NewScriptBuilder().AddData(contract).Script() + if err != nil { + // Should never be hit since this script does exceed the limits. + panic(err) + } + contractPushSize := len(contractPush) + + // 12 additional bytes are for version, locktime and expiry. + return 12 + wire.VarIntSerializeSize(1) + + wire.VarIntSerializeSize(uint64(len(txOuts))) + + inputSize(redeemAtomicSwapSigScriptSize+contractPushSize) + + sumOutputSerializeSizes(txOuts) +} + +// estimateRefundSerializeSize returns a worst case serialize size estimates for +// a transaction that refunds an atomic swap P2SH output. +func estimateRefundSerializeSize(contract []byte, txOuts []*wire.TxOut) int { + contractPush, err := txscript.NewScriptBuilder().AddData(contract).Script() + if err != nil { + // Should never be hit since this script does exceed the limits. + panic(err) + } + contractPushSize := len(contractPush) + + // 12 additional bytes are for version, locktime and expiry. + return 12 + wire.VarIntSerializeSize(1) + + wire.VarIntSerializeSize(uint64(len(txOuts))) + + inputSize(refundAtomicSwapSigScriptSize+contractPushSize) + + sumOutputSerializeSizes(txOuts) +}