Line data Source code
1 : // Copyright (c) 2020-2021 The PIVX Core developers
2 : // Distributed under the MIT software license, see the accompanying
3 : // file COPYING or https://www.opensource.org/licenses/mit-license.php.
4 : #include "key.h"
5 : #include "key_io.h"
6 : #include "policy/policy.h"
7 : #include "wallet/test/wallet_test_fixture.h"
8 : #include "wallet/wallet.h"
9 :
10 : #include <boost/test/unit_test.hpp>
11 :
12 : BOOST_FIXTURE_TEST_SUITE(script_P2CS_tests, WalletTestingSetup)
13 :
14 4 : void CheckValidKeyId(const CTxDestination& dest, const CKeyID& expectedKey)
15 : {
16 4 : const CKeyID* keyid = boost::get<CKeyID>(&dest);
17 4 : if (keyid) {
18 8 : BOOST_CHECK(keyid);
19 8 : BOOST_CHECK(*keyid == expectedKey);
20 : } else {
21 0 : BOOST_ERROR("Destination is not a CKeyID");
22 : }
23 4 : }
24 :
25 : // Goal: check cold staking script keys extraction
26 2 : BOOST_AUTO_TEST_CASE(extract_cold_staking_destination_keys)
27 : {
28 1 : CKey ownerKey;
29 1 : ownerKey.MakeNewKey(true);
30 1 : CKeyID ownerId = ownerKey.GetPubKey().GetID();
31 2 : CKey stakerKey;
32 1 : stakerKey.MakeNewKey(true);
33 1 : CKeyID stakerId = stakerKey.GetPubKey().GetID();
34 2 : CScript script = GetScriptForStakeDelegation(stakerId, ownerId);
35 :
36 : // Check owner
37 2 : CTxDestination ownerDest;
38 2 : BOOST_CHECK(ExtractDestination(script, ownerDest, false));
39 1 : CheckValidKeyId(ownerDest, ownerId);
40 :
41 : // Check staker
42 2 : CTxDestination stakerDest;
43 2 : BOOST_CHECK(ExtractDestination(script, stakerDest, true));
44 1 : CheckValidKeyId(stakerDest, stakerId);
45 :
46 : // Now go with ExtractDestinations.
47 1 : txnouttype type;
48 1 : int nRequiredRet = -1;
49 2 : std::vector<CTxDestination> destVector;
50 2 : BOOST_CHECK(ExtractDestinations(script, type, destVector, nRequiredRet));
51 2 : BOOST_CHECK(type == TX_COLDSTAKE);
52 2 : BOOST_CHECK(nRequiredRet == 2);
53 2 : BOOST_CHECK(destVector.size() == 2);
54 1 : CheckValidKeyId(destVector[0], stakerId);
55 1 : CheckValidKeyId(destVector[1], ownerId);
56 1 : }
57 :
58 2 : static CScript GetNewP2CS(CKey& stakerKey, CKey& ownerKey, bool fLastOutFree)
59 : {
60 4 : stakerKey = KeyIO::DecodeSecret("YNdsth3BsW53DYmCiR12SofWSAt2utXQUSGoin3PekVQCMbzfS7E");
61 4 : ownerKey = KeyIO::DecodeSecret("YUo8oW3y8cUQdQxQxCdnUJ4Ww5H7nHBEMwD2bNDpBbuLM59t4rvd");
62 1 : return fLastOutFree ? GetScriptForStakeDelegationLOF(stakerKey.GetPubKey().GetID(), ownerKey.GetPubKey().GetID())
63 3 : : GetScriptForStakeDelegation(stakerKey.GetPubKey().GetID(), ownerKey.GetPubKey().GetID());
64 : }
65 :
66 2 : static CScript GetDummyP2CS(const CKeyID& dummyKeyID)
67 : {
68 2 : return GetScriptForStakeDelegation(dummyKeyID, dummyKeyID);
69 : }
70 :
71 2 : static CScript GetDummyP2PKH(const CKeyID& dummyKeyID)
72 : {
73 4 : return GetScriptForDestination(dummyKeyID);
74 : }
75 :
76 : static const CAmount amtIn = 200 * COIN;
77 : static const unsigned int flags = STANDARD_SCRIPT_VERIFY_FLAGS;
78 :
79 2 : static CMutableTransaction CreateNewColdStakeTx(CScript& scriptP2CS, CKey& stakerKey, CKey& ownerKey, bool fLastOutFree)
80 : {
81 2 : scriptP2CS = GetNewP2CS(stakerKey, ownerKey, fLastOutFree);
82 :
83 : // Create prev transaction:
84 2 : CMutableTransaction txFrom;
85 2 : txFrom.vout.resize(1);
86 2 : txFrom.vout[0].nValue = amtIn;
87 2 : txFrom.vout[0].scriptPubKey = scriptP2CS;
88 :
89 : // Create coldstake
90 2 : CMutableTransaction tx;
91 2 : tx.vin.resize(1);
92 2 : tx.vout.resize(2);
93 2 : tx.vin[0].prevout.n = 0;
94 2 : tx.vin[0].prevout.hash = txFrom.GetHash();
95 2 : tx.vout[0].nValue = 0;
96 2 : tx.vout[0].scriptPubKey.clear();
97 2 : tx.vout[1].nValue = amtIn + 2 * COIN;
98 2 : tx.vout[1].scriptPubKey = scriptP2CS;
99 :
100 2 : return tx;
101 : }
102 :
103 22 : void SignColdStake(CMutableTransaction& tx, int nIn, const CScript& prevScript, const CKey& key, bool fStaker)
104 : {
105 22 : assert(nIn < (int) tx.vin.size());
106 22 : tx.vin[nIn].scriptSig.clear();
107 22 : const CTransaction _tx(tx);
108 22 : SigVersion sv = _tx.GetRequiredSigVersion();
109 22 : const uint256& hash = SignatureHash(prevScript, _tx, nIn, SIGHASH_ALL, amtIn, sv);
110 44 : std::vector<unsigned char> vchSig;
111 44 : BOOST_CHECK(key.Sign(hash, vchSig));
112 22 : vchSig.push_back((unsigned char)SIGHASH_ALL);
113 51 : std::vector<unsigned char> selector(1, fStaker ? (int) OP_TRUE : OP_FALSE);
114 44 : tx.vin[nIn].scriptSig << vchSig << selector << ToByteVector(key.GetPubKey());
115 22 : }
116 :
117 22 : static bool CheckP2CSScript(const CScript& scriptSig, const CScript& scriptPubKey, const CMutableTransaction& tx, ScriptError& err)
118 : {
119 22 : err = SCRIPT_ERR_OK;
120 44 : return VerifyScript(scriptSig, scriptPubKey, flags, MutableTransactionSignatureChecker(&tx, 0, amtIn), tx.GetRequiredSigVersion(), &err);
121 : }
122 :
123 2 : BOOST_AUTO_TEST_CASE(coldstake_lof_script)
124 : {
125 1 : CScript scriptP2CS;
126 3 : CKey stakerKey, ownerKey;
127 :
128 : // create unsigned coinstake transaction
129 2 : CMutableTransaction good_tx = CreateNewColdStakeTx(scriptP2CS, stakerKey, ownerKey, true);
130 :
131 : // sign the input with the staker key
132 1 : SignColdStake(good_tx, 0, scriptP2CS, stakerKey, true);
133 :
134 : // check the signature and script
135 1 : ScriptError err = SCRIPT_ERR_OK;
136 1 : CMutableTransaction tx(good_tx);
137 2 : BOOST_CHECK(CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
138 :
139 : // pay less than expected
140 1 : tx.vout[1].nValue -= 3 * COIN;
141 1 : SignColdStake(tx, 0, scriptP2CS, stakerKey, true);
142 2 : BOOST_CHECK(!CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
143 2 : BOOST_CHECK_MESSAGE(err == SCRIPT_ERR_CHECKCOLDSTAKEVERIFY, ScriptErrorString(err));
144 :
145 : // Add another p2cs out
146 1 : tx.vout.emplace_back(3 * COIN, scriptP2CS);
147 1 : SignColdStake(tx, 0, scriptP2CS, stakerKey, true);
148 2 : BOOST_CHECK(CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
149 :
150 2 : const CKey& dummyKey = KeyIO::DecodeSecret("YNdsth3BsW53DYmCiR12SofWSAt2utXQUSGoin3PekVQCMbzfS7E");
151 1 : const CKeyID& dummyKeyID = dummyKey.GetPubKey().GetID();
152 2 : const CScript& dummyP2PKH = GetDummyP2PKH(dummyKeyID);
153 :
154 : // Add a masternode out
155 1 : tx.vout.emplace_back(3 * COIN, dummyP2PKH);
156 1 : SignColdStake(tx, 0, scriptP2CS, stakerKey, true);
157 2 : BOOST_CHECK(CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
158 :
159 : // Transfer more coins to the masternode
160 1 : tx.vout[2].nValue -= 3 * COIN;
161 1 : tx.vout[3].nValue += 3 * COIN;
162 1 : SignColdStake(tx, 0, scriptP2CS, stakerKey, true);
163 2 : BOOST_CHECK(!CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
164 2 : BOOST_CHECK_MESSAGE(err == SCRIPT_ERR_CHECKCOLDSTAKEVERIFY, ScriptErrorString(err));
165 :
166 : // Add two "free" outputs
167 1 : tx = good_tx;
168 1 : tx.vout[1].nValue -= 3 * COIN;
169 1 : tx.vout.emplace_back(3 * COIN, dummyP2PKH);
170 1 : tx.vout.emplace_back(3 * COIN, dummyP2PKH);
171 1 : SignColdStake(tx, 0, scriptP2CS, stakerKey, true);
172 2 : BOOST_CHECK(!CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
173 2 : BOOST_CHECK_MESSAGE(err == SCRIPT_ERR_CHECKCOLDSTAKEVERIFY, ScriptErrorString(err));
174 : // -- but the owner can
175 1 : SignColdStake(tx, 0, scriptP2CS, ownerKey, false);
176 2 : BOOST_CHECK(CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
177 :
178 : // Replace with new p2cs
179 1 : tx = good_tx;
180 1 : tx.vout[1].scriptPubKey = GetDummyP2CS(dummyKeyID);
181 1 : SignColdStake(tx, 0, scriptP2CS, stakerKey, true);
182 2 : BOOST_CHECK(!CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
183 2 : BOOST_CHECK_MESSAGE(err == SCRIPT_ERR_CHECKCOLDSTAKEVERIFY, ScriptErrorString(err));
184 : // -- but the owner can
185 1 : SignColdStake(tx, 0, scriptP2CS, ownerKey, false);
186 2 : BOOST_CHECK(CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
187 :
188 : // Replace with single dummy out
189 1 : tx = good_tx;
190 2 : tx.vout[1] = CTxOut(COIN, dummyP2PKH);
191 1 : SignColdStake(tx, 0, scriptP2CS, stakerKey, true);
192 2 : BOOST_CHECK(!CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
193 2 : BOOST_CHECK_MESSAGE(err == SCRIPT_ERR_CHECKCOLDSTAKEVERIFY, ScriptErrorString(err));
194 : // -- but the owner can
195 1 : SignColdStake(tx, 0, scriptP2CS, ownerKey, false);
196 2 : BOOST_CHECK(CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
197 1 : }
198 :
199 2 : BOOST_AUTO_TEST_CASE(coldstake_script)
200 : {
201 1 : CScript scriptP2CS;
202 3 : CKey stakerKey, ownerKey;
203 :
204 : // create unsigned coinstake transaction
205 2 : CMutableTransaction good_tx = CreateNewColdStakeTx(scriptP2CS, stakerKey, ownerKey, false);
206 :
207 : // sign the input with the staker key
208 1 : SignColdStake(good_tx, 0, scriptP2CS, stakerKey, true);
209 :
210 : // check the signature and script
211 1 : ScriptError err = SCRIPT_ERR_OK;
212 1 : CMutableTransaction tx(good_tx);
213 2 : BOOST_CHECK(CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
214 :
215 : // pay less than expected
216 1 : tx.vout[1].nValue -= 3 * COIN;
217 1 : SignColdStake(tx, 0, scriptP2CS, stakerKey, true);
218 2 : BOOST_CHECK(!CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
219 2 : BOOST_CHECK_MESSAGE(err == SCRIPT_ERR_CHECKCOLDSTAKEVERIFY, ScriptErrorString(err));
220 :
221 : // Add another p2cs out
222 1 : tx.vout.emplace_back(3 * COIN, scriptP2CS);
223 1 : SignColdStake(tx, 0, scriptP2CS, stakerKey, true);
224 2 : BOOST_CHECK(CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
225 :
226 2 : const CKey& dummyKey = KeyIO::DecodeSecret("YNdsth3BsW53DYmCiR12SofWSAt2utXQUSGoin3PekVQCMbzfS7E");
227 1 : const CKeyID& dummyKeyID = dummyKey.GetPubKey().GetID();
228 2 : const CScript& dummyP2PKH = GetDummyP2PKH(dummyKeyID);
229 :
230 : // Add a dummy P2PKH out at the end
231 1 : tx.vout.emplace_back(3 * COIN, dummyP2PKH);
232 1 : SignColdStake(tx, 0, scriptP2CS, stakerKey, true);
233 2 : BOOST_CHECK(!CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
234 2 : BOOST_CHECK_MESSAGE(err == SCRIPT_ERR_CHECKCOLDSTAKEVERIFY, ScriptErrorString(err));
235 : // -- but the owner can
236 1 : SignColdStake(tx, 0, scriptP2CS, ownerKey, false);
237 2 : BOOST_CHECK(CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
238 :
239 : // Add a dummy P2PKH out at the beginning
240 1 : tx = good_tx;
241 2 : tx.vout[1] = CTxOut(3 * COIN, dummyP2PKH);
242 1 : tx.vout.emplace_back(3 * COIN, scriptP2CS);
243 1 : SignColdStake(tx, 0, scriptP2CS, stakerKey, true);
244 2 : BOOST_CHECK(!CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
245 2 : BOOST_CHECK_MESSAGE(err == SCRIPT_ERR_CHECKCOLDSTAKEVERIFY, ScriptErrorString(err));
246 : // -- but the owner can
247 1 : SignColdStake(tx, 0, scriptP2CS, ownerKey, false);
248 2 : BOOST_CHECK(CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
249 :
250 : // Replace with new p2cs
251 1 : tx = good_tx;
252 1 : tx.vout[1].scriptPubKey = GetDummyP2CS(dummyKeyID);
253 1 : SignColdStake(tx, 0, scriptP2CS, stakerKey, true);
254 2 : BOOST_CHECK(!CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
255 2 : BOOST_CHECK_MESSAGE(err == SCRIPT_ERR_CHECKCOLDSTAKEVERIFY, ScriptErrorString(err));
256 : // -- but the owner can
257 1 : SignColdStake(tx, 0, scriptP2CS, ownerKey, false);
258 2 : BOOST_CHECK(CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
259 :
260 : // Replace with single dummy out
261 1 : tx = good_tx;
262 2 : tx.vout[1] = CTxOut(COIN, dummyP2PKH);
263 1 : SignColdStake(tx, 0, scriptP2CS, stakerKey, true);
264 2 : BOOST_CHECK(!CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
265 2 : BOOST_CHECK_MESSAGE(err == SCRIPT_ERR_CHECKCOLDSTAKEVERIFY, ScriptErrorString(err));
266 : // -- but the owner can
267 1 : SignColdStake(tx, 0, scriptP2CS, ownerKey, false);
268 2 : BOOST_CHECK(CheckP2CSScript(tx.vin[0].scriptSig, scriptP2CS, tx, err));
269 1 : }
270 :
271 : // Check that it's not possible to "fake" a P2CS script for the owner by splitting the locking
272 : // and unlocking parts. This particular script can be spent by any key, with a
273 : // unlocking script composed like: <sig> <pk> <DUP> <HASH160> <pkh>
274 1 : static CScript GetFakeLockingScript(const CKeyID staker, const CKeyID& owner)
275 : {
276 1 : CScript script;
277 1 : script << opcodetype(0x2F) << opcodetype(0x01) << OP_ROT <<
278 1 : OP_IF << OP_CHECKCOLDSTAKEVERIFY << ToByteVector(staker) <<
279 2 : OP_ELSE << ToByteVector(owner) << OP_DROP <<
280 1 : OP_EQUALVERIFY << OP_CHECKSIG;
281 :
282 1 : return script;
283 : }
284 :
285 0 : void FakeUnlockColdStake(CMutableTransaction& tx, const CScript& prevScript, const CKey& key)
286 : {
287 : // sign the first input
288 0 : tx.vin[0].scriptSig.clear();
289 0 : const CTransaction _tx(tx);
290 0 : SigVersion sv = _tx.GetRequiredSigVersion();
291 0 : const uint256& hash = SignatureHash(prevScript, _tx, 0, SIGHASH_ALL, amtIn, sv);
292 0 : std::vector<unsigned char> vchSig;
293 0 : BOOST_CHECK(key.Sign(hash, vchSig));
294 0 : vchSig.push_back((unsigned char)SIGHASH_ALL);
295 0 : tx.vin[0].scriptSig << vchSig << ToByteVector(key.GetPubKey()) << OP_DUP << OP_HASH160 << ToByteVector(key.GetPubKey().GetID());
296 0 : }
297 :
298 1 : static void setupWallet(CWallet& wallet)
299 : {
300 1 : wallet.SetMinVersion(FEATURE_SAPLING);
301 1 : wallet.SetupSPKM(false);
302 1 : }
303 :
304 2 : BOOST_AUTO_TEST_CASE(fake_script_test)
305 : {
306 1 : CWallet& wallet = m_wallet;
307 1 : LOCK(wallet.cs_wallet);
308 1 : setupWallet(wallet);
309 2 : CKey stakerKey; // dummy staker key (not in the wallet)
310 1 : stakerKey.MakeNewKey(true);
311 1 : CKeyID stakerId = stakerKey.GetPubKey().GetID();
312 1 : CPubKey ownerPubKey;
313 1 : BOOST_ASSERT(wallet.GetKeyFromPool(ownerPubKey));
314 1 : const CKeyID& ownerId = ownerPubKey.GetID();
315 2 : CKey ownerKey; // owner key (in the wallet)
316 1 : BOOST_ASSERT(wallet.GetKey(ownerId, ownerKey));
317 :
318 2 : const CScript& scriptP2CS = GetFakeLockingScript(stakerId, ownerId);
319 :
320 : // Create prev transaction
321 2 : CMutableTransaction txFrom;
322 1 : txFrom.vout.resize(1);
323 1 : txFrom.vout[0].nValue = amtIn;
324 1 : txFrom.vout[0].scriptPubKey = scriptP2CS;
325 :
326 : // it does NOT pass IsPayToColdStaking
327 2 : BOOST_CHECK_MESSAGE(!scriptP2CS.IsPayToColdStaking(), "Fake script passes as P2CS");
328 :
329 : // the output amount is NOT credited to the owner wallet
330 2 : wallet.AddToWallet({&wallet, MakeTransactionRef(CTransaction(txFrom))});
331 1 : BOOST_CHECK_EQUAL(wallet.GetWalletTx(txFrom.GetHash())->GetAvailableCredit(false, ISMINE_SPENDABLE_TRANSPARENT), 0);
332 1 : }
333 :
334 : BOOST_AUTO_TEST_SUITE_END()
|