-
Notifications
You must be signed in to change notification settings - Fork 92
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
295 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} | ||
} | ||
} |