diff --git a/client/wallet/dcr/contract.go b/client/wallet/dcr/contract.go new file mode 100644 index 0000000000..b23c2b01df --- /dev/null +++ b/client/wallet/dcr/contract.go @@ -0,0 +1,214 @@ +// Copyright (c) 2016 The btcsuite developers +// Copyright (c) 2016-2019 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package dcr + +import ( + "crypto/rand" + "crypto/sha256" + "fmt" + + "github.com/decred/dcrd/chaincfg" + "github.com/decred/dcrd/dcrutil" + "github.com/decred/dcrd/txscript" + "github.com/decred/dcrd/wire" + "golang.org/x/crypto/ripemd160" +) + +const ( + secretSize = 32 +) + +// 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() +} + +// sha254Hash returns a sha256 hash of the provided bytes. +func sha256Hash(x []byte) []byte { + h := sha256.Sum256(x) + return h[:] +} + +// newSecret generated a random 32-byte secret. +func newSecret() ([]byte, error) { + var secret [secretSize]byte + _, err := rand.Read(secret[:]) + if err != nil { + return nil, err + } + + return secret[:], nil +} + +// generateContractP2SHPkScript creates a P2SHPk script of the provided contract. +func generateContractP2SHPkScript(contract []byte, params *chaincfg.Params) ([]byte, error) { + contractP2SH, err := dcrutil.NewAddressScriptHash(contract, params) + if err != nil { + return nil, err + } + + contractP2SHPkScript, err := txscript.PayToAddrScript(contractP2SH) + if err != nil { + return nil, err + } + + return contractP2SHPkScript, nil +} + +// Contract represents the swap contract details. +type Contract struct { + Secret []byte + RedeemAddr dcrutil.Address + CounterPartyAddr dcrutil.Address + ContractData []byte + LockTime int64 +} + +// newContract creates a swap contract from the provided address and lock time. +func newContract(redeemAddr string, counterpartyAddr string, lockTime int64) (*Contract, error) { + if lockTime < 0 || uint32(lockTime) > wire.MaxTxInSequenceNum { + return nil, fmt.Errorf("lockTime out of range") + } + + secret, err := newSecret() + if err != nil { + return nil, err + } + + addrMe, err := dcrutil.DecodeAddress(redeemAddr) + if err != nil { + return nil, err + } + + addrThem, err := dcrutil.DecodeAddress(counterpartyAddr) + if err != nil { + return nil, err + } + + secretHash := sha256Hash(secret) + + contract, err := atomicSwapContract(addrMe.Hash160(), addrThem.Hash160(), + lockTime, secretHash) + if err != nil { + return nil, err + } + + return &Contract{ + Secret: secret, + RedeemAddr: addrMe, + CounterPartyAddr: addrThem, + ContractData: contract, + LockTime: lockTime, + }, nil +} + +// generateContracts create contracts from the provided redeem and counterparty addresses. +func generateContracts(redeemAddrs []string, counterpartyAddrs []string, lockTime int64) ([]*Contract, error) { + if len(counterpartyAddrs) == 0 { + return nil, fmt.Errorf("no counterparty addresses provided") + } + + if len(redeemAddrs) == 0 { + return nil, fmt.Errorf("no redeem addresses provided") + } + + if len(redeemAddrs) != len(counterpartyAddrs) { + return nil, fmt.Errorf("counterparty addresses size should be equal" + + " to the redeem addresses size") + } + + contracts := make([]*Contract, len(redeemAddrs)) + for idx, addr := range redeemAddrs { + ctpAddr := counterpartyAddrs[idx] + contract, err := newContract(addr, ctpAddr, lockTime) + if err != nil { + return nil, fmt.Errorf("unable to create contract: %v", err) + } + + contracts[idx] = contract + } + + return contracts, nil +} diff --git a/client/wallet/dcr/contract_test.go b/client/wallet/dcr/contract_test.go new file mode 100644 index 0000000000..40f0233f48 --- /dev/null +++ b/client/wallet/dcr/contract_test.go @@ -0,0 +1,81 @@ +package dcr + +import ( + "bytes" + "testing" + + "github.com/decred/dcrd/dcrutil" +) + +func TestGenerateContracts(t *testing.T) { + tests := []struct { + redeemAddrs []string + ctpAddrs []string + wantErr bool + }{ + { + redeemAddrs: []string{ + "DsaRVfWLBrGYpvUW6o8jDS3iSoHQQcx1wx2", + "DsnHdhaBNtaahvjDPAgmsMCb9QH6LW69XsU", + }, + ctpAddrs: []string{ + "DsmEiHKgE1PkovBCB6xkFGDLxdhhXQ31sgV", + "DsYy9NLvAmhbpBPSDrfF87GJyZ9rSm9XXP3", + }, + wantErr: false, + }, + { + redeemAddrs: []string{ + "DsaRVfWLBrGYpvUW6o8jDS3iSoHQQcx1wx2", + "DsnHdhaBNtaahvjDPAgmsMCb9QH6LW69XsU", + }, + ctpAddrs: []string{ + "DsmEiHKgE1PkovBCB6xkFGDLxdhhXQ31sgV", + }, + wantErr: true, + }, + } + + for idx, tc := range tests { + contracts, err := generateContracts(tc.redeemAddrs, tc.ctpAddrs, 100) + if (err != nil) != tc.wantErr { + t.Fatalf("[generateContracts] #%d: error: %v, wantErr: %v", + idx+1, err, tc.wantErr) + } + + if !tc.wantErr { + for i, addr := range tc.redeemAddrs { + redeemAddr, err := dcrutil.DecodeAddress(addr) + if err != nil { + t.Fatalf("unexpected redeem address err %v", addr) + } + + ctpAddr, err := dcrutil.DecodeAddress(tc.ctpAddrs[i]) + if err != nil { + t.Fatalf("unexpected counterparty address err %v", addr) + } + + contract := contracts[i] + if contract.RedeemAddr.String() != redeemAddr.String() { + t.Fatalf("expected %s redeem address for contract", addr) + } + + if contract.CounterPartyAddr.String() != ctpAddr.String() { + t.Fatalf("expected %s counterparty address for contract", ctpAddr) + } + + expectedContractData, err := atomicSwapContract(redeemAddr.Hash160(), ctpAddr.Hash160(), + contract.LockTime, sha256Hash(contract.Secret)) + if err != nil { + t.Fatalf("unexpected contract error %v", err) + } + + if !bytes.Equal(expectedContractData, contract.ContractData) { + t.Fatalf("generated contract data (%x) is not equal to "+ + " expected contract data (%x)", expectedContractData, + contract.ContractData) + } + } + } + } +}