From 16c16cb60600d3ea761a60d47cbb1c73de954819 Mon Sep 17 00:00:00 2001 From: James Lovejoy Date: Fri, 29 Sep 2017 22:23:33 -0400 Subject: [PATCH 1/3] Implement vtcatomicswap, adding Vertcoin support --- Gopkg.lock | 20 +- README.md | 7 +- cmd/vtcatomicswap/main.go | 1106 ++++++++++++++++++++++++++++++++++ cmd/vtcatomicswap/sizeest.go | 93 +++ 4 files changed, 1221 insertions(+), 5 deletions(-) create mode 100644 cmd/vtcatomicswap/main.go create mode 100644 cmd/vtcatomicswap/sizeest.go diff --git a/Gopkg.lock b/Gopkg.lock index 54d16b5..79f28e8 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -100,6 +100,24 @@ revision = "d2214fcebbf4ae65a0b5a06f9a3cfee02794c565" source = "github.com/jrick/btcwallet" +[[projects]] + branch = "master" + name = "github.com/vertcoin/vtcd" + packages = ["btcec","btcjson","chaincfg","chaincfg/chainhash","rpcclient","txscript","wire"] + revision = "15387ac36fe71840d251e71f4c81110c793e3b9e" + +[[projects]] + branch = "master" + name = "github.com/vertcoin/vtcutil" + packages = [".","base58","bech32"] + revision = "264de6df16ae9bcc556487a3ecd06a132224e6ba" + +[[projects]] + branch = "master" + name = "github.com/vertcoin/vtcwallet" + packages = ["wallet/txrules"] + revision = "b52e3e2c5f5292992b702d991d0b187223873353" + [[projects]] branch = "master" name = "golang.org/x/crypto" @@ -139,6 +157,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "bed7de4b3c807980a327e32ccb1393629c591d73c8297124dcbca3b820fe65a8" + inputs-digest = "802ce4224edbb8eeeb8d26b8cbd2e04b10044a2bf51c7c47f7286d6020f9d5b6" solver-name = "gps-cdcl" solver-version = 1 diff --git a/README.md b/README.md index d88bdda..e584065 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,8 @@ This repo contains utilities to manually perform cross-chain atomic swaps between Decred and other cryptocurrencies. At the moment, Bitcoin (Bitcoin -Core) and Litecoin (Litecoin Core) are the two other blockchains and wallets -supported. Support for other blockchains or wallets could be added in the -future. +Core), Litecoin (Litecoin Core) and Vertcoin (Vertcoin Core) are the three other blockchains and wallets supported. Support for other blockchains or wallets +could be added in the future. These tools do not operate solely on-chain. A side-channel is required between each party performing the swap in order to exchange additional data. This @@ -382,7 +381,7 @@ Several steps require working with a raw transaction published by the other party. While the transactions can sometimes be looked up from a local node using the `getrawtransaction` JSON-RPC, this method can be unreliable since the set of queryable transactions depends on the current UTXO set (bitcoind, -litecoind) or may require the transaction index to be enabled (dcrd). +litecoind, vertcoind) or may require the transaction index to be enabled (dcrd). Another method of discovering these transactions is to use a public blockchain explorer. Not all explorers expose this info through the main user interface so diff --git a/cmd/vtcatomicswap/main.go b/cmd/vtcatomicswap/main.go new file mode 100644 index 0000000..928813d --- /dev/null +++ b/cmd/vtcatomicswap/main.go @@ -0,0 +1,1106 @@ +// Copyright (c) 2017 The Decred developers. +// 2017 The Vertcoin 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" + "encoding/hex" + "encoding/json" + "errors" + "flag" + "fmt" + "net" + "os" + "strconv" + "strings" + "time" + + "github.com/vertcoin/vtcd/chaincfg" + "github.com/vertcoin/vtcd/chaincfg/chainhash" + rpc "github.com/vertcoin/vtcd/rpcclient" + "github.com/vertcoin/vtcd/txscript" + "github.com/vertcoin/vtcd/wire" + "github.com/vertcoin/vtcutil" + "github.com/vertcoin/vtcwallet/wallet/txrules" + "golang.org/x/crypto/ripemd160" +) + +const verify = true + +const secretSize = 32 + +const txVersion = 2 + +var ( + chainParams = &chaincfg.VertcoinParams +) + +var ( + flagset = flag.NewFlagSet("", flag.ExitOnError) + connectFlag = flagset.String("s", "localhost", "host[:port] of Vertcoin 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 +// Vertcoin 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_RIPEMD160 and OP_CHECKLOCKTIMEVERIFY. +// +// Example scenerios using Vertcoin as the second chain: +// +// Scenerio 1: +// cp1 initiates (dcr) +// cp2 participates with cp1 H(S) (vtc) +// cp1 redeems vtc revealing S +// - must verify H(S) in contract is hash of known secret +// cp2 redeems dcr with S +// +// Scenerio 2: +// cp1 initiates (vtc) +// 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 vtc with S + +func init() { + flagset.Usage = func() { + fmt.Println("Usage: vtcatomicswap [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 *vtcutil.AddressPubKeyHash + amount vtcutil.Amount +} + +type participateCmd struct { + cp1Addr *vtcutil.AddressPubKeyHash + amount vtcutil.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.VertcoinTestNetParams + } + + var cmd command + switch args[0] { + case "initiate": + cp2Addr, err := vtcutil.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.(*vtcutil.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 := vtcutil.NewAmount(amountF64) + if err != nil { + return err, true + } + + cmd = &initiateCmd{cp2Addr: cp2AddrP2PKH, amount: amount} + + case "participate": + cp1Addr, err := vtcutil.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.(*vtcutil.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 := vtcutil.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) != ripemd160.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) != ripemd160.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.VertcoinParams: + return "5888" + case &chaincfg.VertcoinTestNetParams: + return "15888" + default: + return "" + } +} + +// createSig creates and returns the serialized raw signature and compressed +// pubkey for a transaction input signature. Due to limitations of the Vertcoin +// 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 vtcutil.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 +// vtcd/rpcclient package. +func fundRawTransaction(c *rpc.Client, tx *wire.MsgTx, feePerKb vtcutil.Amount) (fundedTx *wire.MsgTx, fee vtcutil.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 := vtcutil.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 vtcutil.Amount, err error) { + info, err := c.GetInfo() + if err != nil { + return 0, 0, fmt.Errorf("getinfo: %v", err) + } + relayFee, err = vtcutil.NewAmount(info.RelayFee) + if err != nil { + return 0, 0, err + } + maxFee := info.PaytxFee + if info.PaytxFee != 0 { + if info.RelayFee > maxFee { + maxFee = info.RelayFee + } + useFee, err = vtcutil.NewAmount(maxFee) + return useFee, relayFee, err + } + + params := []json.RawMessage{[]byte("6")} + estimateRawResp, err := c.RawRequest("estimatefee", params) + if err != nil { + return 0, 0, err + } + var estimateResp float64 = -1 + err = json.Unmarshal(estimateRawResp, &estimateResp) + if err == nil && estimateResp != -1 { + useFee, err = vtcutil.NewAmount(estimateResp) + if relayFee > useFee { + useFee = relayFee + } + return useFee, relayFee, err + } + + fmt.Println("warning: falling back to mempool relay fee policy") + useFee, err = vtcutil.NewAmount(info.RelayFee) + return useFee, relayFee, err +} + +// getRawChangeAddress calls the getrawchangeaddress JSON-RPC method. +func getRawChangeAddress(c *rpc.Client) (vtcutil.Address, error) { + rawResp, err := c.RawRequest("getrawchangeaddress", nil) + if err != nil { + return nil, err + } + var addrStr string + err = json.Unmarshal(rawResp, &addrStr) + if err != nil { + return nil, err + } + addr, err := vtcutil.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) + } + 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 *vtcutil.AddressPubKeyHash + amount vtcutil.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 vtcutil.Address + contractTxHash *chainhash.Hash + contractTx *wire.MsgTx + contractFee vtcutil.Amount + refundTx *wire.MsgTx + refundFee vtcutil.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 := vtcutil.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 vtcutil.Amount) ( + refundTx *wire.MsgTx, refundFee vtcutil.Amount, err error) { + + contractP2SH, err := vtcutil.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(contract) + if err != nil { + // expected to only be called with good input + panic(err) + } + + refundAddr, err := vtcutil.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", vtcutil.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 ripemd160Hash(x []byte) []byte { + h := ripemd160.New() + h.Write(x) + return h.Sum(nil) +} + +func calcFeePerKb(absoluteFee vtcutil.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 := ripemd160Hash(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 VTC/kB)\n", b.contractFee, contractFeePerKb) + fmt.Printf("Refund fee: %v (%0.8f VTC/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 VTC/kB)\n", b.contractFee, contractFeePerKb) + fmt.Printf("Refund fee: %v (%0.8f VTC/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(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 := vtcutil.NewAddressPubKeyHash(pushes.RecipientHash160[:], + chainParams) + if err != nil { + return err + } + contractHash := vtcutil.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].(*vtcutil.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", vtcutil.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 VTC/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(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 VTC/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(ripemd160Hash(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 := vtcutil.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].(*vtcutil.AddressScriptHash).Hash160()[:], contractHash160) { + contractOut = i + break + } + } + if contractOut == -1 { + return errors.New("transaction does not contain the contract output") + } + + pushes, err := txscript.ExtractAtomicSwapDataPushes(cmd.contract) + if err != nil { + return err + } + if pushes == nil { + return errors.New("contract is not an atomic swap script recognized by this tool") + } + + contractAddr, err := vtcutil.NewAddressScriptHash(cmd.contract, chainParams) + if err != nil { + return err + } + recipientAddr, err := vtcutil.NewAddressPubKeyHash(pushes.RecipientHash160[:], + chainParams) + if err != nil { + return err + } + refundAddr, err := vtcutil.NewAddressPubKeyHash(pushes.RefundHash160[:], + chainParams) + if err != nil { + return err + } + + fmt.Printf("Contract address: %v\n", contractAddr) + fmt.Printf("Contract value: %v\n", vtcutil.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 known to redeem the output. A + // ripemd160 hash is used here as it is the only shared hash opcode + // between decred and vertcoin. + b.AddOp(txscript.OP_RIPEMD160) + 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/vtcatomicswap/sizeest.go b/cmd/vtcatomicswap/sizeest.go new file mode 100644 index 0000000..60c2e44 --- /dev/null +++ b/cmd/vtcatomicswap/sizeest.go @@ -0,0 +1,93 @@ +// Copyright (c) 2016 The btcsuite developers +// 2016-2017 The Decred developers +// 2017 The Vertcoin 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/vertcoin/vtcd/txscript" + "github.com/vertcoin/vtcd/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) +} From eed0b8358c93d42785d9c5f720521cbd04d84f13 Mon Sep 17 00:00:00 2001 From: James Lovejoy Date: Sat, 30 Sep 2017 16:40:37 -0400 Subject: [PATCH 2/3] Fix copyright comments and readme word wrapping --- README.md | 11 ++++++----- cmd/vtcatomicswap/main.go | 3 ++- cmd/vtcatomicswap/sizeest.go | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e584065..653cc96 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,9 @@ This repo contains utilities to manually perform cross-chain atomic swaps between Decred and other cryptocurrencies. At the moment, Bitcoin (Bitcoin -Core), Litecoin (Litecoin Core) and Vertcoin (Vertcoin Core) are the three other blockchains and wallets supported. Support for other blockchains or wallets -could be added in the future. +Core), Litecoin (Litecoin Core) and Vertcoin (Vertcoin Core) are the three +other blockchains and wallets supported. Support for other blockchains or +wallets could be added in the future. These tools do not operate solely on-chain. A side-channel is required between each party performing the swap in order to exchange additional data. This @@ -14,9 +15,9 @@ and a way for early adopters to try out the technology. Due to the requirements of manually exchanging data and creating, sending, and watching for the relevant transactions, it is highly recommended to read this -README in its entirety before attempting to use these tools. The sections below -explain the principles on which the tools operate, the instructions for how to -use them safely, and an example swap between Decred and Bitcoin. +README in its entirety before attempting to use these tools. The sections +below explain the principles on which the tools operate, the instructions for +how to use them safely, and an example swap between Decred and Bitcoin. ## Build instructions diff --git a/cmd/vtcatomicswap/main.go b/cmd/vtcatomicswap/main.go index 928813d..adcad01 100644 --- a/cmd/vtcatomicswap/main.go +++ b/cmd/vtcatomicswap/main.go @@ -1,5 +1,6 @@ // Copyright (c) 2017 The Decred developers. -// 2017 The Vertcoin developers. +// Copyright (c) 2017 The Vertcoin developers. +// // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. diff --git a/cmd/vtcatomicswap/sizeest.go b/cmd/vtcatomicswap/sizeest.go index 60c2e44..6c7d395 100644 --- a/cmd/vtcatomicswap/sizeest.go +++ b/cmd/vtcatomicswap/sizeest.go @@ -1,6 +1,6 @@ // Copyright (c) 2016 The btcsuite developers -// 2016-2017 The Decred developers -// 2017 The Vertcoin developers +// Copyright (c) 2016-2017 The Decred developers +// Copyright (c) 2017 The Vertcoin developers // // Use of this source code is governed by an ISC // license that can be found in the LICENSE file. From 0e7d957c5f09f4f8febc99007a422eb0e7a6de32 Mon Sep 17 00:00:00 2001 From: James Lovejoy Date: Fri, 6 Oct 2017 01:51:39 -0400 Subject: [PATCH 3/3] Update Gopkg.toml with Vertcoin deps --- Gopkg.toml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Gopkg.toml b/Gopkg.toml index 60ffbda..4be8c85 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -67,3 +67,16 @@ [[constraint]] name = "google.golang.org/grpc" version = "1.6.0" + +[[projects]] + branch = "master" + name = "github.com/vertcoin/vtcd" + +[[projects]] + branch = "master" + name = "github.com/vertcoin/vtcutil" + +[[projects]] + branch = "master" + name = "github.com/vertcoin/vtcwallet" +