diff --git a/server/asset/dcr/cache.go b/server/asset/dcr/cache.go index 812e8df1cf..f76f9a43ca 100644 --- a/server/asset/dcr/cache.go +++ b/server/asset/dcr/cache.go @@ -12,10 +12,7 @@ import ( ) // The dcrBlock structure should hold a minimal amount of information about a -// block needed to verify UTXO validity. The stake related transaction hashes -// are stored for quick tx tree checks. More sophisticated methods such as -// examining the pubkey scripts may be used in the future to make this structure -// even smaller. +// block needed to verify UTXO validity. type dcrBlock struct { hash chainhash.Hash height int64 diff --git a/server/asset/dcr/config.go b/server/asset/dcr/config.go index d343aa4e09..6c9de50b41 100644 --- a/server/asset/dcr/config.go +++ b/server/asset/dcr/config.go @@ -32,7 +32,7 @@ var ( // DCRConfig is passed to the constructor. type DCRConfig struct { // Net should one of "mainnet", "testnet3", "simnet". Any other value will - // cause an tidyConfig error. + // cause a tidyConfig error. Net string // DcrdUser is the RPC username provided to dcrd configuration as the rpcuser // parameter. diff --git a/server/asset/dcr/dcr.go b/server/asset/dcr/dcr.go index c7a7e452e8..88ca627f68 100644 --- a/server/asset/dcr/dcr.go +++ b/server/asset/dcr/dcr.go @@ -41,7 +41,7 @@ type dcrNode interface { GetBlockHash(blockHeight int64) (*chainhash.Hash, error) } -// dcrBackend is an asset backend for Decred. It has utilities for fetching UTXO +// dcrBackend is an asset backend for Decred. It has methods for fetching UTXO // information and subscribing to block updates. It maintains a cache of block // data for quick lookups. dcrBackend implements asset.DEXAsset, so provides // exported methods for DEX-related blockchain info. @@ -127,6 +127,8 @@ func (dcr *dcrBackend) UTXO(txid string, vout uint32, redeemScript []byte) (asse return dcr.utxo(txHash, vout, redeemScript) } +// Transaction is part of the asset.DEXTx interface, so includes methods for +// checking spent utxos and validating swap contracts. func (dcr *dcrBackend) Transaction(txid string) (asset.DEXTx, error) { txHash, err := chainhash.NewHashFromStr(txid) if err != nil { @@ -270,7 +272,8 @@ func (dcr *dcrBackend) onBlockConnected(serializedHeader []byte, _ [][]byte) { log.Errorf("error decoding serialized header: %v", err) return } - dcr.anyQ <- blockHeader.BlockHash() + h := blockHeader.BlockHash() + dcr.anyQ <- &h } // Get the UTXO, populating the block data along the way. Only spendable UTXOs @@ -303,8 +306,7 @@ func (dcr *dcrBackend) utxo(txHash *chainhash.Hash, vout uint32, redeemScript [] return nil, fmt.Errorf("error parsing utxo script addresses") } - // Most supported script types are P2PKH, with or without a leading stake-tree - // byte, e.g. OP_SSGEN, OP_SSRTX + // Most supported script types are P2PKH. sigScriptSize := P2PKHSigScriptSize // If it's a P2SH, the size must be calculated based on other factors. if scriptType.isP2SH() { @@ -355,7 +357,10 @@ func (dcr *dcrBackend) utxo(txHash *chainhash.Hash, vout uint32, redeemScript [] pkScript: pkScript, redeemScript: redeemScript, numSigs: scriptAddrs.nRequired, - size: uint32(sigScriptSize), + // The total size associated with the wire.TxIn. See + // (wire.TxIn).SerializeSizeWitness and + // and (wire.TxIn).SerializeSizePrefix + spendSize: uint32(sigScriptSize) + txInOverhead, }, nil } @@ -442,7 +447,7 @@ func (dcr *dcrBackend) VerifySignature(msg, pkBytes, sigBytes []byte) bool { } // connectNodeRPC attempts to create a new websocket connection to a dcrd node -// with the given credentials and optional notification handlers. +// with the given credentials and notification handlers. func connectNodeRPC(host, user, pass, cert string, notifications *rpcclient.NotificationHandlers) (*rpcclient.Client, error) { diff --git a/server/asset/dcr/dcr_test.go b/server/asset/dcr/dcr_test.go index 5fd22e69af..0ea36aeff2 100644 --- a/server/asset/dcr/dcr_test.go +++ b/server/asset/dcr/dcr_test.go @@ -97,13 +97,13 @@ func TestTidyConfig(t *testing.T) { } } -// The remaining tests use the testBlockchain which, is a stub for +// The remaining tests use the testBlockchain which is a stub for // rpcclient.Client. UTXOs, transactions and blocks are added to the blockchain // as jsonrpc types to be requested by the dcrBackend. // // General formula for testing -// 1. Create a dcrBackend with the node field set to a dcrNode-implementing stub -// 2. Create a fake UTXO and all of the necessary jsonrpc-type blocks and +// 1. Create a dcrBackend with the node field set to a testNode +// 2. Create a fake UTXO and all of the associated jsonrpc-type blocks and // transactions and add the to the test blockchain. // 3. Verify the dcrBackend and UTXO methods are returning whatever is expected. // 4. Optionally add more blocks and/or transactions to the blockchain and check @@ -124,7 +124,7 @@ func randomHash() *chainhash.Hash { return hash } -// A fake blockchain to be used for RPC calls by the dcrNode. +// A fake "blockchain" to be used for RPC calls by the dcrNode. type testBlockChain struct { txOuts map[string]*chainjson.GetTxOutResult txRaws map[chainhash.Hash]*chainjson.TxRawResult @@ -136,7 +136,8 @@ type testBlockChain struct { // node stub to request. var testChain testBlockChain -// This must be called before using the testNode. +// This must be called before using the testNode, and should be called +// in-between independent tests. func cleanTestChain() { testChain = testBlockChain{ txOuts: make(map[string]*chainjson.GetTxOutResult), @@ -303,7 +304,7 @@ func genPubkey() ([]byte, []byte) { // A pay-to-script-hash pubkey script. func newP2PKHScript() ([]byte, []byte, []byte) { - script := make([]byte, 0, SwapContractSize) + script := make([]byte, 0, P2PKHSigScriptSize) script = addHex(script, "76a914") pubkey, pkHash := genPubkey() script = append(script, pkHash...) @@ -335,6 +336,7 @@ func testMsgTxRegular() *testMsgTx { } } +// Information about a swap contract. type testMsgTxSwap struct { tx *wire.MsgTx vout uint32 @@ -345,7 +347,7 @@ type testMsgTxSwap struct { // Create a swap (initialization) contract with random pubkeys and return the // pubkey script and addresses. func testSwapContract() ([]byte, dcrutil.Address) { - contract := make([]byte, 0, SwapContractSize) + contract := make([]byte, 0, 25) contract = addHex(contract, "6382012088c020") // This snippet checks the size of the secret and hashes it. // hashed secret secretKey := randomBytes(32) @@ -362,6 +364,7 @@ func testSwapContract() ([]byte, dcrutil.Address) { return contract, receiverAddr } +// Create a transaction with a P2SH swap output at vout 0. func testMsgTxSwapInit() *testMsgTxSwap { msgTx := wire.NewMsgTx() contract, recipient := testSwapContract() @@ -410,6 +413,7 @@ func testMsgTxVote() *testMsgTx { } } +// Information about a transaction with a P2SH output. type testMsgTxP2SH struct { tx *wire.MsgTx pubkeys [][]byte @@ -558,7 +562,7 @@ func TestUTXOs(t *testing.T) { } // While we're here, check the spend script size is correct. scriptSize := utxo.ScriptSize() - if scriptSize != P2PKHSigScriptSize { + if scriptSize != P2PKHSigScriptSize+txInOverhead { t.Fatalf("case 1 - unexpected spend script size reported. expected %d, got %d", P2PKHSigScriptSize, scriptSize) } // Now "mine" the transaction. @@ -1136,5 +1140,4 @@ func TestAuxiliary(t *testing.T) { if utxo.TxID() != txid { t.Fatalf("utxo txid doesn't match") } - } diff --git a/server/asset/dcr/live_test.go b/server/asset/dcr/live_test.go index 786f87447c..45e60dff49 100644 --- a/server/asset/dcr/live_test.go +++ b/server/asset/dcr/live_test.go @@ -1,6 +1,8 @@ // +build dcrlive // -// These tests can be run by using the -tags flag. +// Since at least one live test runs for an hour, you should run live tests +// individually using the -run flag. All of these tests will only run with the +// 'dcrlive' build tag, specified with the -tags flag. // // go test -v -tags dcrlive -run LiveUTXO // ----------------------------------- @@ -12,6 +14,11 @@ // go test -v -tags dcrlive -run CacheAdvantage // ----------------------------------------- // Check the difference between using the block cache and requesting via RPC. +// +// go test -v -tags dcrlive -run BlockMonitor -timeout 61m +// ------------------------------------------ +// Monitor the block chain for a while and make sure that the block cache is +// updating appropriately. package dcr @@ -318,3 +325,37 @@ func TestCacheAdvantage(t *testing.T) { } t.Logf("%d cached blocks retreived in %.3f ms", numBlocks, float64(time.Since(start).Nanoseconds())/1e6) } + +// TestBlockMonitor is a live test that connects to dcrd and listens for block +// updates, checking the state of the cache along the way. +func TestBlockMonitor(t *testing.T) { + testDuration := 60 * time.Minute + fmt.Printf("Starting BlockMonitor test. Test will last for %d minutes\n", int(testDuration.Minutes())) + blockChan := dcr.BlockChannel(5) + expire := time.NewTimer(testDuration).C + lastHeight := dcr.blockCache.tipHeight() +out: + for { + select { + case height := <-blockChan: + if height > lastHeight { + t.Logf("block received for height %d", height) + } else { + reorgDepth := lastHeight - height + 1 + t.Logf("block received for block %d causes a %d block reorg", height, reorgDepth) + } + tipHeight := dcr.blockCache.tipHeight() + if tipHeight != height { + t.Fatalf("unexpected height after block notification. expected %d, received %d", height, tipHeight) + } + _, err := dcr.getMainchainDcrBlock(height) + if err != nil { + t.Fatalf("error getting newly connected block at height %d", height) + } + case <-dcr.ctx.Done(): + break out + case <-expire: + break out + } + } +} diff --git a/server/asset/dcr/script.go b/server/asset/dcr/script.go index a401abbb31..8ac0ea52e3 100644 --- a/server/asset/dcr/script.go +++ b/server/asset/dcr/script.go @@ -56,6 +56,8 @@ const ( P2PKHSigScriptSize = 1 + 73 + 1 + 33 // All pubkey scripts are assumed to be version 0. currentScriptVersion = 0 + // Overhead for a wire.TxIn with a script length < 254. + txInOverhead = 58 ) // parseScriptType creates a dcrScriptType bitmap for the script type. A script diff --git a/server/asset/dcr/tx.go b/server/asset/dcr/tx.go index 9ed16e3013..39ee538e27 100644 --- a/server/asset/dcr/tx.go +++ b/server/asset/dcr/tx.go @@ -103,7 +103,7 @@ func (tx *Tx) Confirmations() (int64, error) { mainchainBlock = nil } else { mainchainBlock, found = tx.dcr.blockCache.atHeight(uint32(tx.height)) - if !found { + if !found || mainchainBlock.hash != tx.blockHash { return -1, fmt.Errorf("new block not found for utxo moved from orphaned block") } } diff --git a/server/asset/dcr/utxo.go b/server/asset/dcr/utxo.go index c38fe198d5..b5539dde6f 100644 --- a/server/asset/dcr/utxo.go +++ b/server/asset/dcr/utxo.go @@ -27,7 +27,7 @@ type UTXO struct { pkScript []byte redeemScript []byte numSigs int - size uint32 + spendSize uint32 } // Check that UTXO satisfies the asset.UTXO interface @@ -156,7 +156,7 @@ func countMatches(pubkeys [][]byte, addrs []dcrutil.Address, hasher func([]byte) // ScriptSize returns the maximum spend script size of the UTXO, in bytes. // This is a method of the asset.UTXO interface. func (utxo *UTXO) ScriptSize() uint32 { - return utxo.size + return utxo.spendSize } // TxHash is the transaction hash. TxHash is a method of the asset.UTXO