Skip to content

Commit c5c6497

Browse files
committed
Merge #350: Make block solutions more strictly validated
1348313 add basic functional tests for signed block rpcs and validation (Gregory Sanders) c07b0af Add signature checking flag for when no sighash byte is expected (Gregory Sanders) 8f92b21 remove block (multi)signature malleability vectors (Gregory Sanders)
2 parents 884005b + 1348313 commit c5c6497

File tree

5 files changed

+159
-6
lines changed

5 files changed

+159
-6
lines changed

qa/pull-tester/rpc-tests.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@
164164
'rpcnamedargs.py',
165165
'listsinceblock.py',
166166
'p2p-leaktests.py',
167+
'signed_blockchain.py',
167168
]
168169
if ENABLE_ZMQ:
169170
testScripts.append('zmq_test.py')

qa/rpc-tests/signed_blockchain.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2014-2016 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
6+
#
7+
# Test RPC calls related to blockchain state. Tests correspond to code in
8+
# rpc/blockchain.cpp.
9+
#
10+
11+
from decimal import Decimal
12+
from test_framework.test_framework import BitcoinTestFramework
13+
from test_framework.authproxy import JSONRPCException
14+
from test_framework.util import (
15+
assert_equal,
16+
start_nodes,
17+
assert_raises_jsonrpc,
18+
)
19+
20+
21+
class SignedBlockchainTest(BitcoinTestFramework):
22+
"""
23+
Test signed-blockchain-related RPC calls:
24+
25+
- getnewblockhex
26+
- signblock
27+
- combineblocksigs
28+
- submitblock
29+
30+
"""
31+
32+
def __init__(self):
33+
super().__init__()
34+
self.setup_clean_chain = True
35+
self.num_nodes = 3
36+
37+
def setup_network(self, split=False):
38+
# Keys for signing 2-of-3
39+
pubkeys = ["039560e48d4336e40db447fc136ce24ae1dfdefa5701e4d4e57aa1a1a9f47f3faa", "025a66517c1d85adcd909f9f675bf656708edc4da1f614693e347be6baf0fef4ae", "02ca238faeb3b01d26ae8a39869220dbd84cc8516398afa8958fe613fe4fdf1c04"]
40+
# Normal multisig scriptPubKey: 1 <33 byte pubkey> <33 byte pubkey> ... 2 OP_CMS
41+
sign_script = "-signblockscript=5221"+pubkeys[0]+"21"+pubkeys[1]+"21"+pubkeys[2]+"53ae"
42+
self.nodes = start_nodes(self.num_nodes, self.options.tmpdir, [[sign_script],[sign_script], [sign_script]])
43+
# nodes are disconnected for this test
44+
self.is_network_split = True
45+
46+
def run_test(self):
47+
keys = ["cRANrxPMceu8jKAA76xzpA9PtTEhBknyZyaaQZ3Z5FnFTGkCAqmT", "cTJRjDBWo1JXdie31B5wv5eXNrPGCAjQKn48umubhmNLjsnj951V", "cVjTXVgKE8PhvCgDowRJVQW68q7j5kSbDGfAT5CUwt9D8dn2cAwf"]
48+
assert_equal(self.nodes[0].getblockcount(), 0)
49+
assert_equal(self.nodes[1].getblockcount(), 0)
50+
51+
block_hex = self.nodes[0].getnewblockhex()
52+
53+
# Block needs signatures, but valid and extends chaintip otherwise
54+
assert_equal(self.nodes[0].testproposedblock(block_hex), None)
55+
assert_equal(self.nodes[1].testproposedblock(block_hex), None)
56+
57+
assert_equal(self.nodes[0].submitblock(block_hex), "block-proof-invalid")
58+
assert_equal(self.nodes[1].submitblock(block_hex), "block-proof-invalid")
59+
60+
assert_equal(self.nodes[0].getblockcount(), 0)
61+
assert_equal(self.nodes[1].getblockcount(), 0)
62+
63+
# combineblocksigs only returns true when signatures are appended and enough
64+
# are included to pass validation
65+
assert_equal(self.nodes[0].combineblocksigs(block_hex, [])["complete"], False)
66+
assert_equal(self.nodes[1].combineblocksigs(block_hex, [])["complete"], False)
67+
68+
# Now we can try to sign, without key
69+
assert_equal(self.nodes[0].signblock(block_hex), "00")
70+
assert_equal(self.nodes[1].signblock(block_hex), "00")
71+
72+
# Import keys
73+
self.nodes[0].importprivkey(keys[0])
74+
self.nodes[1].importprivkey(keys[1])
75+
self.nodes[2].importprivkey(keys[2])
76+
77+
sig0 = self.nodes[0].signblock(block_hex)
78+
sig1 = self.nodes[1].signblock(block_hex)
79+
80+
combined0 = self.nodes[0].combineblocksigs(block_hex, [sig0])
81+
assert(not combined0["complete"])
82+
combined1 = self.nodes[0].combineblocksigs(combined0["hex"], [sig1])
83+
assert(combined1["complete"])
84+
85+
# Still haven't moved forward
86+
assert_equal(self.nodes[0].getblockcount(), 0)
87+
assert_equal(self.nodes[1].getblockcount(), 0)
88+
89+
self.nodes[0].submitblock(combined1["hex"])
90+
# Move his chain along for later
91+
self.nodes[2].submitblock(combined1["hex"])
92+
93+
assert_equal(self.nodes[0].getblockcount(), 1)
94+
assert_equal(self.nodes[1].getblockcount(), 0)
95+
assert_equal(self.nodes[2].getblockcount(), 1)
96+
97+
block_too_far = self.nodes[0].getnewblockhex()
98+
99+
assert_raises_jsonrpc,(JSONRPCException, -25, "proposal was not based on our best chain", self.nodes[1].testproposedblock, block_too_far)
100+
101+
# Finally, submit block
102+
self.nodes[1].submitblock(combined1["hex"])
103+
assert_equal(self.nodes[1].getblockcount(), 1)
104+
105+
# Now proposal is fine, aside from sigs
106+
assert_equal(self.nodes[1].testproposedblock(block_too_far), None)
107+
sig0 = self.nodes[0].signblock(block_too_far)
108+
sig1 = self.nodes[1].signblock(block_too_far)
109+
sig2 = self.nodes[2].signblock(block_too_far)
110+
111+
combined0 = self.nodes[0].combineblocksigs(block_too_far, [sig0])
112+
assert(not combined0["complete"])
113+
# combining signature from third node this time
114+
combined1 = self.nodes[0].combineblocksigs(combined0["hex"], [sig2])
115+
assert(combined1["complete"])
116+
117+
# TODO stuff with too many signatures or junk data manually
118+
119+
if __name__ == '__main__':
120+
SignedBlockchainTest().main()

src/pow.cpp

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,28 @@ bool CheckProof(const CBlockHeader& block, const Consensus::Params& params)
6161
{
6262
if (block.GetHash() == params.hashGenesisBlock)
6363
return true;
64-
return GenericVerifyScript(block.proof.solution, block.proof.challenge, SCRIPT_VERIFY_P2SH, block);
64+
65+
// Some important anti-DoS flags.
66+
// Note: Blockhashes do not commit to the proof.
67+
// Therefore we may have a signature be mealleated
68+
// to stay valid, but cause the block to fail
69+
// validation, in this case, block weight.
70+
// In that case, the block will be marked as permanently
71+
// invalid and not processed.
72+
// NOTE: These have only been deemed sufficient for OP_CMS
73+
// ANY OTHER SCRIPT TYPE MAY REQUIRE DIFFERENT FLAGS/CONSIDERATIONS
74+
// TODO: Better design to not have to worry about script specifics
75+
// i.e. exempt block header solution from weight limit
76+
unsigned int proof_flags = SCRIPT_VERIFY_P2SH // Just allows P2SH evaluation
77+
| SCRIPT_VERIFY_STRICTENC // Minimally-sized DER sigs
78+
| SCRIPT_VERIFY_NULLDUMMY // No extra data stuffed into OP_CMS witness
79+
| SCRIPT_VERIFY_CLEANSTACK // No extra pushes leftover in witness
80+
| SCRIPT_VERIFY_MINIMALDATA // Pushes are minimally-sized
81+
| SCRIPT_VERIFY_SIGPUSHONLY // Witness is push-only
82+
| SCRIPT_VERIFY_LOW_S // Stop easiest signature fiddling
83+
| SCRIPT_VERIFY_WITNESS // Required for cleanstack eval in VerifyScript
84+
| SCRIPT_NO_SIGHASH_BYTE; // non-Check(Multi)Sig signatures will not have sighash byte
85+
return GenericVerifyScript(block.proof.solution, block.proof.challenge, proof_flags, block);
6586
}
6687

6788
bool MaybeGenerateProof(CBlockHeader *pblock, CWallet *pwallet)

src/script/interpreter.cpp

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -223,12 +223,19 @@ bool CheckSignatureEncoding(const vector<unsigned char> &vchSig, unsigned int fl
223223
if (vchSig.size() == 0) {
224224
return true;
225225
}
226-
if ((flags & (SCRIPT_VERIFY_DERSIG | SCRIPT_VERIFY_LOW_S | SCRIPT_VERIFY_STRICTENC)) != 0 && !IsValidSignatureEncoding(vchSig)) {
226+
bool no_hash_byte = (flags & SCRIPT_NO_SIGHASH_BYTE) == SCRIPT_NO_SIGHASH_BYTE;
227+
std::vector<unsigned char> vchSigCopy(vchSig.begin(), vchSig.begin() + vchSig.size());
228+
// Push a dummy sighash byte to pass checks
229+
if (no_hash_byte) {
230+
vchSigCopy.push_back(SIGHASH_ALL);
231+
}
232+
233+
if ((flags & (SCRIPT_VERIFY_DERSIG | SCRIPT_VERIFY_LOW_S | SCRIPT_VERIFY_STRICTENC)) != 0 && !IsValidSignatureEncoding(vchSigCopy)) {
227234
return set_error(serror, SCRIPT_ERR_SIG_DER);
228-
} else if ((flags & SCRIPT_VERIFY_LOW_S) != 0 && !IsLowDERSignature(vchSig, serror)) {
235+
} else if ((flags & SCRIPT_VERIFY_LOW_S) != 0 && !IsLowDERSignature(vchSigCopy, serror)) {
229236
// serror is set
230237
return false;
231-
} else if ((flags & SCRIPT_VERIFY_STRICTENC) != 0 && !IsDefinedHashtypeSignature(vchSig)) {
238+
} else if ((flags & SCRIPT_VERIFY_STRICTENC) != 0 && !IsDefinedHashtypeSignature(vchSigCopy)) {
232239
return set_error(serror, SCRIPT_ERR_SIG_HASHTYPE);
233240
}
234241
return true;
@@ -1313,8 +1320,8 @@ bool EvalScript(vector<vector<unsigned char> >& stack, const CScript& script, un
13131320
valtype& vchData = stacktop(-2);
13141321
valtype& vchPubKey = stacktop(-1);
13151322

1316-
// Sigs from stack have no hash type, so we disable strictenc check
1317-
if (!CheckSignatureEncoding(vchSig, (flags & ~SCRIPT_VERIFY_STRICTENC), serror) || !CheckPubKeyEncoding(vchPubKey, flags, sigversion, serror)) {
1323+
// Sigs from stack have no hash byte ever
1324+
if (!CheckSignatureEncoding(vchSig, (flags | SCRIPT_NO_SIGHASH_BYTE), serror) || !CheckPubKeyEncoding(vchPubKey, flags, sigversion, serror)) {
13181325
//serror is set
13191326
return false;
13201327
}

src/script/interpreter.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ enum
107107
// Public keys in segregated witness scripts must be compressed
108108
//
109109
SCRIPT_VERIFY_WITNESS_PUBKEYTYPE = (1U << 15),
110+
111+
// Signature checking assumes no sighash byte after the DER signature
112+
//
113+
SCRIPT_NO_SIGHASH_BYTE = (1U << 16),
110114

111115
};
112116

0 commit comments

Comments
 (0)