diff --git a/src/avalanche/proof.h b/src/avalanche/proof.h --- a/src/avalanche/proof.h +++ b/src/avalanche/proof.h @@ -16,6 +16,11 @@ #include #include +/** Minimum amount per utxo */ +static constexpr Amount PROOF_DUST_THRESOLD = 1 * COIN; +/** Minimum total amount */ +static constexpr Amount PROOF_MIN_TOTAL_AMOUNT = 10 * COIN; + class CCoinsView; namespace avalanche { diff --git a/src/avalanche/proof.cpp b/src/avalanche/proof.cpp --- a/src/avalanche/proof.cpp +++ b/src/avalanche/proof.cpp @@ -47,19 +47,23 @@ return uint32_t((100 * total) / COIN); } -static constexpr Amount PROOF_DUST_THRESOLD = 1 * SATOSHI; - bool Proof::verify(ProofValidationState &state) const { if (stakes.empty()) { return state.Invalid(ProofValidationResult::NO_STAKE); } - + Amount totalAmount = Amount::zero(); std::unordered_set utxos; for (const SignedStake &ss : stakes) { const Stake &s = ss.getStake(); if (s.getAmount() < PROOF_DUST_THRESOLD) { return state.Invalid(ProofValidationResult::DUST_THRESOLD); } + // Verify the total amount makes sense: in MoneyRange, no overflow + if (!MoneyRange(s.getAmount()) || + !MoneyRange(totalAmount + s.getAmount())) { + return state.Invalid(ProofValidationResult::INVALID_TOTAL_AMOUNT); + } + totalAmount += s.getAmount(); if (!utxos.insert(s.getUTXO()).second) { return state.Invalid(ProofValidationResult::DUPLICATE_STAKE); @@ -69,6 +73,9 @@ return state.Invalid(ProofValidationResult::INVALID_SIGNATURE); } } + if (totalAmount < PROOF_MIN_TOTAL_AMOUNT) { + return state.Invalid(ProofValidationResult::INSUFFICIENT_TOTAL_AMOUNT); + } return true; } diff --git a/src/avalanche/test/peermanager_tests.cpp b/src/avalanche/test/peermanager_tests.cpp --- a/src/avalanche/test/peermanager_tests.cpp +++ b/src/avalanche/test/peermanager_tests.cpp @@ -162,13 +162,13 @@ const NodeId node0 = 42, node1 = 69, node2 = 37; // One peer, we always return it. - Proof proof0 = buildRandomProof(100); + Proof proof0 = buildRandomProof(MIN_SCORE); Delegation dg0 = DelegationBuilder(proof0).build(); pm.addNode(node0, proof0, dg0); BOOST_CHECK_EQUAL(pm.selectNode(), node0); // Two peers, verify ratio. - Proof proof1 = buildRandomProof(200); + Proof proof1 = buildRandomProof(2 * MIN_SCORE); Delegation dg1 = DelegationBuilder(proof1).build(); pm.addNode(node1, proof1, dg1); @@ -182,7 +182,7 @@ BOOST_CHECK(abs(2 * results[0] - results[1]) < 500); // Three peers, verify ratio. - Proof proof2 = buildRandomProof(100); + Proof proof2 = buildRandomProof(MIN_SCORE); Delegation dg2 = DelegationBuilder(proof2).build(); pm.addNode(node2, proof2, dg2); diff --git a/src/avalanche/test/proof_tests.cpp b/src/avalanche/test/proof_tests.cpp --- a/src/avalanche/test/proof_tests.cpp +++ b/src/avalanche/test/proof_tests.cpp @@ -27,8 +27,25 @@ BOOST_CHECK_EQUAL(p.getScore(), score); ProofValidationState state; - BOOST_CHECK(p.verify(state)); - BOOST_CHECK(state.GetResult() == ProofValidationResult::NONE); + if (score >= MIN_SCORE) { + if (score <= MAX_SCORE) { + BOOST_CHECK(p.verify(state)); + BOOST_CHECK(state.GetResult() == ProofValidationResult::NONE); + } else { + BOOST_CHECK(!p.verify(state)); + BOOST_CHECK(state.GetResult() == + ProofValidationResult::INVALID_TOTAL_AMOUNT); + } + } else { + BOOST_CHECK(!p.verify(state)); + if (score >= MIN_SCORE_DUST) { + BOOST_CHECK(state.GetResult() == + ProofValidationResult::DUST_THRESOLD); + } else { + BOOST_CHECK(state.GetResult() == + ProofValidationResult::INSUFFICIENT_TOTAL_AMOUNT); + } + } } } @@ -46,6 +63,7 @@ for (int i = 0; i < 3; i++) { key.MakeNewKey(true); + const Amount v = int64_t(InsecureRand32()) * COIN / 100; pb.addUTXO(COutPoint(TxId(GetRandHash()), InsecureRand32()), int64_t(InsecureRand32()) * COIN / 100, InsecureRand32(), InsecureRandBool(), key); @@ -432,6 +450,28 @@ BOOST_CHECK(state.GetResult() == ProofValidationResult::DUST_THRESOLD); } + // Dust thresold + { + ProofBuilder pb(0, 0, pubkey); + pb.addUTXO(pkh_outpoint, Amount::zero(), height, false, key); + Proof p = pb.build(); + + ProofValidationState state; + BOOST_CHECK(!p.verify(state, coins)); + BOOST_CHECK(state.GetResult() == ProofValidationResult::DUST_THRESOLD); + } + + { + ProofBuilder pb(0, 0, pubkey); + pb.addUTXO(pkh_outpoint, PROOF_DUST_THRESOLD - 1 * Amount::satoshi(), + height, false, key); + Proof p = pb.build(); + + ProofValidationState state; + BOOST_CHECK(!p.verify(state, coins)); + BOOST_CHECK(state.GetResult() == ProofValidationResult::DUST_THRESOLD); + } + // Duplicated input { ProofBuilder pb(0, 0, pubkey); @@ -444,6 +484,31 @@ BOOST_CHECK(state.GetResult() == ProofValidationResult::DUPLICATE_STAKE); } + + // Too low total amount for all stakes + { + ProofBuilder pb(0, 0, pubkey); + pb.addUTXO(pkh_outpoint, PROOF_MIN_TOTAL_AMOUNT - 1 * SATOSHI, height, + false, key); + Proof p = pb.build(); + + ProofValidationState state; + BOOST_CHECK(!p.verify(state, coins)); + BOOST_CHECK(state.GetResult() == + ProofValidationResult::INSUFFICIENT_TOTAL_AMOUNT); + } + + // Invalid amount + { + ProofBuilder pb(0, 0, pubkey); + pb.addUTXO(pkh_outpoint, MAX_MONEY + 1 * SATOSHI, height, false, key); + Proof p = pb.build(); + + ProofValidationState state; + BOOST_CHECK(!p.verify(state, coins)); + BOOST_CHECK(state.GetResult() == + ProofValidationResult::INVALID_TOTAL_AMOUNT); + } } BOOST_AUTO_TEST_SUITE_END() diff --git a/src/avalanche/test/util.h b/src/avalanche/test/util.h --- a/src/avalanche/test/util.h +++ b/src/avalanche/test/util.h @@ -10,6 +10,13 @@ #include +// Minimum score to reach a value of PROOF_DUST_THRESHOLD +constexpr uint32_t MIN_SCORE_DUST = PROOF_DUST_THRESOLD / COIN * 100; +// Minimum score to reach a value of PROOF_MIN_TOTAL_AMOUNT +constexpr uint32_t MIN_SCORE = PROOF_MIN_TOTAL_AMOUNT / COIN * 100; +// Minimum score to reach a value of PROOF_MIN_TOTAL_AMOUNT +const uint32_t MAX_SCORE = MAX_MONEY / COIN * 100; + namespace avalanche { Proof buildRandomProof(uint32_t score, const CPubKey &master = CPubKey()); diff --git a/src/avalanche/validation.h b/src/avalanche/validation.h --- a/src/avalanche/validation.h +++ b/src/avalanche/validation.h @@ -15,6 +15,8 @@ DUST_THRESOLD, DUPLICATE_STAKE, INVALID_SIGNATURE, + INVALID_TOTAL_AMOUNT, + INSUFFICIENT_TOTAL_AMOUNT, // UTXO based errors. MISSING_UTXO, diff --git a/src/init.cpp b/src/init.cpp --- a/src/init.cpp +++ b/src/init.cpp @@ -2461,6 +2461,15 @@ InitError(_("the avalanche proof has invalid stake " "signatures")); return false; + case avalanche::ProofValidationResult::INVALID_TOTAL_AMOUNT: + InitError(_("the avalanche proof has an invalid total " + "amount")); + return false; + case avalanche::ProofValidationResult:: + INSUFFICIENT_TOTAL_AMOUNT: + InitError(_("the avalanche proof has an insufficient " + "total amount")); + return false; default: InitError(_("the avalanche proof is invalid")); return false; diff --git a/test/functional/abc_p2p_avalanche.py b/test/functional/abc_p2p_avalanche.py --- a/test/functional/abc_p2p_avalanche.py +++ b/test/functional/abc_p2p_avalanche.py @@ -463,6 +463,28 @@ "f7d2888d96b82962b3ce516d1083c0e031773487fc3c4f2e38acd1db974" "1321b91a79b82d1c2cfd47793261e4ba003cf5") + # This was generated using the same params as "dust", but with an amount + # larger than MAX_MONEY (the buildavalancheproof RPC will fail to + # generate this proof) + invalid_amount = ( + "0b000000000000000c0000000000000021030b4c866585dd868a9d62348a9cd008" + "d6a312937048fff31670e7e920cfc7a7440105c5f72f5d6da3085583e75ee79340" + "eb4eff208c89988e7ed0efb30b87298fa3000000000021fd5ff075070003000000" + "210227d85ba011276cf25b51df6a188b75e604b38770a462b2d0e9fb2fc839ef5d" + "3f8d9bf4daaed14b58db87f5a4f79cc73ca93c392685e332b570c5c57df46c3e77" + "f94977f973045adfdcac1ad9b1306735a25abc8b1dfd95d02f8b5ffa3a483f38") + + insufficient_total_amount = node.buildavalancheproof( + proof_sequence, proof_expiration, pubkey.get_bytes().hex(), + [{ + 'txid': coinbases[0]['txid'], + 'vout': coinbases[0]['n'], + 'amount': '9.0', + 'height': coinbases[0]['height'], + 'iscoinbase': True, + 'privatekey': addrkey0.key, + }]) + self.stop_node(0) def check_proof_init_error(proof, message): @@ -482,6 +504,10 @@ "the avalanche proof has duplicated stake") check_proof_init_error(bad_sig, "the avalanche proof has invalid stake signatures") + check_proof_init_error(invalid_amount, + "the avalanche proof has an invalid total amount") + check_proof_init_error(insufficient_total_amount, + "the avalanche proof has an insufficient total amount") if __name__ == '__main__':