diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -556,6 +556,7 @@ avalanche/delegation.cpp avalanche/delegationbuilder.cpp avalanche/peermanager.cpp + avalanche/postconsensus.cpp avalanche/processor.cpp avalanche/proof.cpp avalanche/proofid.cpp diff --git a/src/avalanche/postconsensus.h b/src/avalanche/postconsensus.h new file mode 100644 --- /dev/null +++ b/src/avalanche/postconsensus.h @@ -0,0 +1,34 @@ +// Copyright (c) 2022 The Bitcoin developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_AVALANCHE_POSTCONSENSUS_H +#define BITCOIN_AVALANCHE_POSTCONSENSUS_H + +#include + +/** + * Parameters and defaults for Avalanche post-consensus policies. + */ + +class ArgsManager; +class CChainParams; + +namespace avalanche { + +/** + * Is early block penalty enabled by default? + */ +static constexpr bool EARLY_BLOCK_PENALTY_DEFAULT_ENABLED = false; +static constexpr int64_t EARLY_BLOCK_PENALTY_FACTOR_DEFAULT = 10; +static constexpr int64_t EARLY_BLOCK_PENALTY_WINDOW_DEFAULT = 2 * 60; + +bool isEarlyBlockPenaltyEnabled(const ArgsManager &argsman); +int64_t getEarlyBlockPenaltyFactor(const ArgsManager &argsman, + const CChainParams &chainparams); +int64_t getEarlyBlockPenaltyWindow(const ArgsManager &argsman, + const CChainParams &chainparams); + +} // namespace avalanche + +#endif // BITCOIN_AVALANCHE_POSTCONSENSUS_H diff --git a/src/avalanche/postconsensus.cpp b/src/avalanche/postconsensus.cpp new file mode 100644 --- /dev/null +++ b/src/avalanche/postconsensus.cpp @@ -0,0 +1,41 @@ +// Copyright (c) 2022 The Bitcoin developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include + +namespace avalanche { + +bool isEarlyBlockPenaltyEnabled(const ArgsManager &argsman) { + return argsman.GetBoolArg("-avapolicyearlyblockpenalty", + EARLY_BLOCK_PENALTY_DEFAULT_ENABLED); +} + +int64_t getEarlyBlockPenaltyFactor(const ArgsManager &argsman, + const CChainParams &chainparams) { + if (chainparams.IsTestChain() && + !argsman.IsArgSet("-avapolicyearlyblockpenaltyfactor")) { + // Test chains apply no penalty by default + return 1; + } + + return argsman.GetIntArg("-avapolicyearlyblockpenaltyfactor", + EARLY_BLOCK_PENALTY_FACTOR_DEFAULT); +} + +int64_t getEarlyBlockPenaltyWindow(const ArgsManager &argsman, + const CChainParams &chainparams) { + if (chainparams.IsTestChain() && + !argsman.IsArgSet("-avapolicyearlyblockpenaltywindow")) { + // Test chains apply no penalty by default + return 0; + } + + return argsman.GetIntArg("-avapolicyearlyblockpenaltywindow", + EARLY_BLOCK_PENALTY_WINDOW_DEFAULT); +} + +} // namespace avalanche diff --git a/src/init.cpp b/src/init.cpp --- a/src/init.cpp +++ b/src/init.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include // For AVALANCHE_LEGACY_PROOF_DEFAULT #include @@ -1426,6 +1427,27 @@ "%u).", DEFAULT_MAX_AVALANCHE_OUTBOUND_CONNECTIONS), ArgsManager::ALLOW_INT, OptionsCategory::AVALANCHE); + argsman.AddArg( + "-avapolicyearlyblockpenalty", + strprintf("Enable the avalanche post-consensus policy to apply a " + "difficulty penalty to early blocks. Early blocks will be " + "voted against but will still finalize if the rest of the " + "network accepts the block. (default: %u)", + avalanche::EARLY_BLOCK_PENALTY_DEFAULT_ENABLED), + ArgsManager::ALLOW_BOOL, OptionsCategory::AVALANCHE); + argsman.AddArg( + "-avapolicyearlyblockpenaltyfactor", + strprintf( + "Difficulty penalty factor applied to early blocks. (default: %u)", + avalanche::EARLY_BLOCK_PENALTY_FACTOR_DEFAULT), + ArgsManager::ALLOW_INT, OptionsCategory::AVALANCHE); + argsman.AddArg( + "-avapolicyearlyblockpenaltywindow", + strprintf("A difficulty penalty is applied if a block is received " + "within the penalty window from the previous block's " + "received time. Unit is seconds (default: %u)", + avalanche::EARLY_BLOCK_PENALTY_WINDOW_DEFAULT), + ArgsManager::ALLOW_INT, OptionsCategory::AVALANCHE); // Add the hidden options argsman.AddHiddenArgs(hidden_args); diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -1053,6 +1053,7 @@ result.pushKV("longpollid", active_chain.Tip()->GetBlockHash().GetHex() + ToString(nTransactionsUpdatedLast)); + // TODO target needs to adjust based on penalty result.pushKV("target", hashTarget.GetHex()); result.pushKV("mintime", int64_t(pindexPrev->GetMedianTimePast()) + 1); diff --git a/src/validation.h b/src/validation.h --- a/src/validation.h +++ b/src/validation.h @@ -864,7 +864,8 @@ bool AcceptBlock(const Config &config, const std::shared_ptr &pblock, BlockValidationState &state, bool fRequested, - const FlatFilePos *dbp, bool *fNewBlock) + const FlatFilePos *dbp, bool *fNewBlock, + bool fPostConsensusActive = false) EXCLUSIVE_LOCKS_REQUIRED(cs_main); // Block (dis)connection on a given view: diff --git a/src/validation.cpp b/src/validation.cpp --- a/src/validation.cpp +++ b/src/validation.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -4250,18 +4251,24 @@ /** * Store a block on disk. * - * @param[in] config The global config. - * @param[in,out] pblock The block we want to accept. - * @param[in] fRequested A boolean to indicate if this block was requested - * from our peers. - * @param[in] dbp If non-null, the disk position of the block. - * @param[in,out] fNewBlock True if block was first received via this call. + * @param[in] config The global config. + * @param[in,out] pblock The block we want to accept. + * @param[in] fRequested A boolean to indicate if this block was + * requested from our peers. + * @param[in] dbp If non-null, the disk position of the + * block. + * @param[in,out] fNewBlock True if block was first received via + * this call. + * @param[in] fPostConsensusActive A boolean to indicate if Avalanche + * post-consensus voting is currently + * active. * @return True if the block is accepted as a valid block and written to disk. */ bool CChainState::AcceptBlock(const Config &config, const std::shared_ptr &pblock, BlockValidationState &state, bool fRequested, - const FlatFilePos *dbp, bool *fNewBlock) { + const FlatFilePos *dbp, bool *fNewBlock, + bool fPostConsensusActive) { AssertLockHeld(cs_main); const CBlock &block = *pblock; @@ -4383,6 +4390,59 @@ } } + // Apply Avalanche post-consensus policies if voting is active + if (fPostConsensusActive && avalanche::isEarlyBlockPenaltyEnabled(gArgs) && + pindex->pprev) { + const CBlockIndex *parent = pindex->pprev; + const int64_t timeDiff = + pindex->nTimeReceived >= parent->nTimeReceived + ? pindex->nTimeReceived - parent->nTimeReceived + : 0; + + const int64_t penaltyWindow = + avalanche::getEarlyBlockPenaltyWindow(gArgs, m_params); + if (timeDiff < penaltyWindow) { + const int64_t penaltyFactor = + avalanche::getEarlyBlockPenaltyFactor(gArgs, m_params); + const CBlockHeader header = pindex->GetBlockHeader(); + + LogPrint(BCLog::AVALANCHE, + "Early block hash: %s, parentTime: %d, receivedTime: %d, " + "timeDiff: %d\n", + header.GetHash().ToString(), parent->nTimeReceived, + pindex->nTimeReceived, timeDiff); + arith_uint256 target; + bool negative; + bool overflow; + const uint32_t powRequired = + GetNextWorkRequired(parent, &header, m_params); + target.SetCompact(powRequired, &negative, &overflow); + const arith_uint256 oldTarget = target; + + target /= penaltyFactor; + const uint32_t newPowLimit = target.GetCompact(negative); + + LogPrint(BCLog::AVALANCHE, + "Early block targets: oldTarget: %s, oldPowLimit: 0x%.8x, " + "newTarget: %s, newPowLimit: 0x%.8x\n", + oldTarget.ToString(), powRequired, target.ToString(), + newPowLimit); + + if (!CheckProofOfWork(header.GetHash(), newPowLimit, + consensusParams)) { + // Mark this block as parked + pindex->nStatus = pindex->nStatus.withParked(); + m_blockman.m_dirty_blockindex.insert(pindex); + LogPrint(BCLog::AVALANCHE, + "Early block rejected due to PoW penalty: %s\n", + header.GetHash().ToString()); + } else { + LogPrint(BCLog::AVALANCHE, "Early block accepted: %s\n", + header.GetHash().ToString()); + } + } + } + // Header is valid/has work and the merkle tree is good. // Relay now, but if it does not build on our best tip, let the // SendMessages loop relay it. @@ -4420,6 +4480,9 @@ bool force_processing, bool *new_block) { AssertLockNotHeld(cs_main); + bool postConsensusActive = + g_avalanche && g_avalanche->isQuorumEstablished(); + { if (new_block) { *new_block = false; @@ -4441,7 +4504,8 @@ if (ret) { // Store to disk ret = ActiveChainstate().AcceptBlock( - config, block, state, force_processing, nullptr, new_block); + config, block, state, force_processing, nullptr, new_block, + postConsensusActive); } if (!ret) { diff --git a/test/functional/abc_p2p_avalanche_policy_early_blocks.py b/test/functional/abc_p2p_avalanche_policy_early_blocks.py new file mode 100755 --- /dev/null +++ b/test/functional/abc_p2p_avalanche_policy_early_blocks.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +# Copyright (c) 2022 The Bitcoin developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test avalanche post-consensus policy: difficulty penalty for early blocks""" +import random +import time + +from test_framework.avatools import get_ava_p2p_interface +from test_framework.blocktools import create_block, create_coinbase +from test_framework.messages import ( + AvalancheVote, + AvalancheVoteError, + msg_block, + uint256_from_compact, +) +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal + +QUORUM_NODE_COUNT = 8 +NUM_BLOCKS_TO_CHECK = 20 + + +class AvalanchePolicyEarlyBlocksTest(BitcoinTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 1 + self.penaltyfactor = 10 + self.penaltywindow = 100 + self.extra_args = [ + [ + '-avalanche=1', + '-avapolicyearlyblockpenalty=1', + f'-avapolicyearlyblockpenaltyfactor={self.penaltyfactor}', + f'-avapolicyearlyblockpenaltywindow={self.penaltywindow}', + '-avaproofstakeutxodustthreshold=1000000', + '-avaproofstakeutxoconfirmations=1', + '-avacooldown=0', + '-avaminquorumstake=0', + '-avaminavaproofsnodecount=0', + '-whitelist=noban@127.0.0.1', + ], + ] + + def run_test(self): + node = self.nodes[0] + + mocktime = int(time.time()) + node.setmocktime(mocktime) + + # Build a fake quorum of nodes. + def get_quorum(): + nonlocal mocktime + + quorum = [] + for _ in range(0, QUORUM_NODE_COUNT): + # Set mocktime far beyond the penalty window so that the block + # generated as part of proof creation is always accepted. + mocktime += self.penaltywindow * 10 + node.setmocktime(mocktime) + quorum.append(get_ava_p2p_interface(self, node)) + + return quorum + + # Pick a node from the quorum for polling. + quorum = get_quorum() + poll_node = quorum[0] + + assert node.getavalancheinfo()['ready_to_poll'] is True + + def assert_response(expected): + r = poll_node.wait_for_avaresponse().response + + votes = r.votes + assert_equal(len(votes), len(expected)) + for i in range(0, len(votes)): + assert_equal(repr(votes[i]), repr(expected[i])) + + # Generate some blocks outside of the penalty window and check them + # against the policy. + bestblockhash = node.getbestblockhash() + tip = int(bestblockhash, 16) + height = node.getblockcount() + 1 + blocktime = node.getblock(bestblockhash)['time'] + 1 + for _ in range(0, NUM_BLOCKS_TO_CHECK): + block = create_block( + tip, + create_coinbase(height), + blocktime, + version=4) + block.solve() + tip = block.sha256 + height += 1 + blocktime += 1 + + # Make block received time just outside of the penalty window. + mocktime += self.penaltywindow + node.setmocktime(mocktime) + poll_node.send_and_ping(msg_block(block)) + + # Blocks outside of the penalty window are accepted. + poll_node.send_poll([tip]) + assert_response([AvalancheVote(AvalancheVoteError.ACCEPTED, tip)]) + + # Generate some test cases and always include the penalty window + # boundaries. + time_deltas = sorted( + random.sample( + range( + 0, + self.penaltywindow), + NUM_BLOCKS_TO_CHECK)) + time_deltas[0] = 0 + time_deltas[-1] = self.penaltywindow - 1 + + # Generate some blocks that may be rejected inside of the penalty + # window. + for blockindex in range(0, NUM_BLOCKS_TO_CHECK): + block = create_block( + tip, + create_coinbase(height), + blocktime, + version=4) + block.solve() + target = uint256_from_compact(block.nBits) + expect_parked = block.sha256 + assert expect_parked <= target + + if expect_parked <= target / self.penaltyfactor: + # Block difficulty exceeds highest possible difficulty penalty, + # so just ignore it + continue + + # Set block received time such that the block is rejected. + node.setmocktime(mocktime + time_deltas[blockindex]) + poll_node.send_and_ping(msg_block(block)) + + poll_node.send_poll([expect_parked]) + assert_response( + [AvalancheVote(AvalancheVoteError.PARKED, expect_parked)]) + + +if __name__ == '__main__': + AvalanchePolicyEarlyBlocksTest().main()