Skip to content

Commit

Permalink
add contract tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
dnldd committed Nov 4, 2019
1 parent 6ce48d3 commit 89498b5
Show file tree
Hide file tree
Showing 2 changed files with 295 additions and 0 deletions.
214 changes: 214 additions & 0 deletions client/wallet/dcr/contract.go
Original file line number Diff line number Diff line change
@@ -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:
//
// <their sig> <their pubkey> <initiator secret> 1
//
// <my sig> <my pubkey> 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
}
81 changes: 81 additions & 0 deletions client/wallet/dcr/contract_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
}

0 comments on commit 89498b5

Please sign in to comment.