diff --git a/src/Makefile.am b/src/Makefile.am index 5bdeb7ce9b..24ae008afc 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -167,6 +167,7 @@ BITCOIN_CORE_H = \ policy/policy.h \ policy/rbf.h \ policy/settings.h \ + pos.h \ pos_kernel.h \ pow.h \ protocol.h \ @@ -287,6 +288,7 @@ libbitcoin_server_a_SOURCES = \ policy/fees.cpp \ policy/rbf.cpp \ policy/settings.cpp \ + pos.cpp \ pos_kernel.cpp \ pow.cpp \ rest.cpp \ diff --git a/src/chain.h b/src/chain.h index 1b67ebbe41..4cc4847d3e 100644 --- a/src/chain.h +++ b/src/chain.h @@ -14,6 +14,7 @@ #include #include +#include /** * Maximum amount of time that a block timestamp is allowed to exceed the @@ -181,6 +182,10 @@ class CBlockIndex uint32_t nTime; uint32_t nBits; uint32_t nNonce; + boost::optional proofOfStakeBody; + + // proof-of-stake specific fields + uint256 stakeModifier; // hash modifier for proof-of-stake //! (memory only) Sequential id assigned to distinguish order in which blocks are received. int32_t nSequenceId; @@ -329,6 +334,11 @@ class CBlockIndex return false; } + bool IsProofOfStake() const + { + return (bool) proofOfStakeBody; + } + //! Build the skiplist pointer for this entry. void BuildSkip(); diff --git a/src/chainparams.cpp b/src/chainparams.cpp index f511be2e68..ec66a23226 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -69,6 +69,17 @@ class CMainParams : public CChainParams { consensus.BIP34Hash = uint256S("0x000000000000024b89b42a942fe0d9fea3bb44ab7bd1b19115dd6a759c0808b8"); consensus.BIP65Height = 388381; // 000000000000000004c2b624ed5d7756c508d90fd0da2c7c679febfa6c4735f0 consensus.BIP66Height = 363725; // 00000000000000000379eaa19dce8c9b722d46ae6a57c2f1a988119488b50931 + + consensus.pos.diffLimit = uint256S("00000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + consensus.pos.nTargetTimespan = 14 * 24 * 60 * 60; // two weeks + consensus.pos.nTargetSpacing = 10 * 60; // 10 minutes + consensus.pos.fAllowMinDifficultyBlocks = false; // only for regtest + consensus.pos.fNoRetargeting = false; // only for regtest + + consensus.pos.coinstakeMaturity = 100; + + consensus.pos.allowMintingWithoutPeers = false; // don't mint if no peers connected + consensus.CSVHeight = 419328; // 000000000000000004a1b34462cb8aeebd5799177f7a29cf28f2d1961716b5b5 consensus.SegwitHeight = 481824; // 0000000000000000001c8018d9cb3b742ef25114f27563e3fc4a1902167f9893 consensus.powLimit = uint256S("00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); @@ -175,6 +186,17 @@ class CTestNetParams : public CChainParams { consensus.BIP34Hash = uint256S("0x0000000023b3a96d3484e5abb3755c413e7d41500f8e2a5c3f0dd01299cd8ef8"); consensus.BIP65Height = 581885; // 00000000007f6655f22f98e72ed80d8b06dc761d5da09df0fa1dc4be4f861eb6 consensus.BIP66Height = 330776; // 000000002104c8c45e99a8853285a3b592602a3ccde2b832481da85e9e4ba182 + + consensus.pos.diffLimit = uint256S("00000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + consensus.pos.nTargetTimespan = 14 * 24 * 60 * 60; // two weeks + consensus.pos.nTargetSpacing = 10 * 60; // 10 minutes + consensus.pos.fAllowMinDifficultyBlocks = false; // only for regtest + consensus.pos.fNoRetargeting = false; // only for regtest + + consensus.pos.coinstakeMaturity = 100; + + consensus.pos.allowMintingWithoutPeers = false; // don't mint if no peers connected + consensus.CSVHeight = 770112; // 00000000025e930139bac5c6c31a403776da130831ab85be56578f3fa75369bb consensus.SegwitHeight = 834624; // 00000000002b980fcd729daaa248fd9316a5200e9b367f4ff2c42453e84201ca consensus.powLimit = uint256S("00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); @@ -259,6 +281,16 @@ class CRegTestParams : public CChainParams { consensus.BIP34Hash = uint256(); consensus.BIP65Height = 1351; // BIP65 activated on regtest (Used in functional tests) consensus.BIP66Height = 1251; // BIP66 activated on regtest (Used in functional tests) + consensus.pos.diffLimit = uint256S("00000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + consensus.pos.nTargetTimespan = 14 * 24 * 60 * 60; // two weeks + consensus.pos.nTargetSpacing = 10 * 60; // 10 minutes + consensus.pos.fAllowMinDifficultyBlocks = false; // only for regtest + consensus.pos.fNoRetargeting = false; // only for regtest + + consensus.pos.coinstakeMaturity = 100; + + consensus.pos.allowMintingWithoutPeers = false; // don't mint if no peers connected + consensus.CSVHeight = 432; // CSV activated on regtest (Used in rpc activation tests) consensus.SegwitHeight = 0; // SEGWIT is always activated on regtest unless overridden consensus.powLimit = uint256S("7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); diff --git a/src/consensus/params.h b/src/consensus/params.h index 8263b0fef4..c903b48a1d 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -10,6 +10,7 @@ #include #include #include +#include namespace Consensus { @@ -62,6 +63,22 @@ struct Params { * Note that segwit v0 script rules are enforced on all blocks except the * BIP 16 exception blocks. */ int SegwitHeight; + + struct PoS { + uint256 diffLimit; + int64_t nTargetTimespan; + int64_t nTargetSpacing; + bool fAllowMinDifficultyBlocks; + bool fNoRetargeting; + + int64_t DifficultyAdjustmentInterval() const { return nTargetTimespan / nTargetSpacing; } + + arith_uint256 interestAtoms = arith_uint256{10000000000000000ull}; + bool allowMintingWithoutPeers; + int coinstakeMaturity = 500; + }; + PoS pos; + /** * Minimum blocks including miner confirmation of the total of 2016 blocks in a retargeting period, * (nPowTargetTimespan / nPowTargetSpacing) which is also used for BIP9 deployments. diff --git a/src/pos.cpp b/src/pos.cpp new file mode 100644 index 0000000000..6e560f046c --- /dev/null +++ b/src/pos.cpp @@ -0,0 +1,136 @@ +#include +#include +#include +#include +#include + +#include +#include + +static bool CheckStakeModifier(const CBlockIndex* pindexPrev, const CBlock& block, const Consensus::Params& params) { + if (block.hashPrevBlock.IsNull()) + return block.stakeModifier.IsNull(); + + if (block.IsProofOfStake()) + return block.stakeModifier == pos::ComputeStakeModifier_PoS(pindexPrev->stakeModifier, + block.proofOfStakeBody->coinstakePrevout); + return block.stakeModifier == pos::ComputeStakeModifier_PoW(pindexPrev->stakeModifier, block.hashPrevBlock); +} + +bool CheckBlockProof_headerOnly(const CBlockHeader& block, const Consensus::Params& params) { + return pos::CheckProofOfStake_headerOnly(block, params); +} + +bool CheckBlockProof(const CBlockIndex* pindexPrev, const CBlock& block, CCoinsViewCache& view, + const Consensus::Params& params) { + if (!CheckStakeModifier(pindexPrev, block, params)) { + return false; + } + + return pos::CheckProofOfStake(pindexPrev, block, view, params); + +} + +namespace pos { + +/// Check PoS signatures (PoS block hashes are signed with coinstake out pubkey) + bool CheckHeaderSignature(const CBlockHeader& block) { + if (!block.IsProofOfStake()) { + return true; + } + + if (block.proofOfStakeBody->sig.empty()) { + LogPrintf("CheckBlockSignature: Bad Block - PoS signature is empty\n"); + return false; + } + + CPubKey recoveredPubKey{}; + if (!recoveredPubKey.RecoverCompact(block.GetHashToSign(), block.proofOfStakeBody->sig)) { + LogPrintf("CheckBlockSignature: Bad Block - malformed signature\n"); + return false; + } + + if (recoveredPubKey.GetID() != block.proofOfStakeBody->pubKeyHash) { + LogPrintf("CheckBlockSignature: Bad Block - wrong signature\n"); + return false; + } + return true; + } + + bool CheckProofOfStake_headerOnly(const CBlockHeader& block, const Consensus::Params& params) { + if (!block.IsProofOfStake()) { + return error("CheckProofOfStake_headerOnly(): called on non-PoS %s", block.GetHash().ToString()); + } + + const int64_t coinstakeTime = (int64_t) block.GetBlockTime(); + const auto& body = *block.proofOfStakeBody; // checked to exist after IsProofOfStake + + // checking PoS kernel is faster, so check it first + if (!CheckKernelHash(block.stakeModifier, block.nBits, coinstakeTime, body.coinstakeAmount, + body.coinstakePrevout, params).hashOk) { + return false; + } + return CheckHeaderSignature(block); + } + + bool CheckProofOfStake(const CBlockIndex* pindexPrev, const CBlock& block, CCoinsViewCache& view, + const Consensus::Params& params) { + if (block.IsProofOfStake() != block.HasCoinstakeTx()) { + return false; // block claimed it's PoS, but doesn't have coinstakeTx + } + if (!block.IsCompleteProofOfStake()) { + return error("CheckProofOfStake(): called on a non-PoS block %s", block.GetHash().ToString()); + } + + const auto& body = *block.proofOfStakeBody; // checked to exist after IsProofOfStake + + if (body.coinstakePrevout != block.vtx[1]->vin[0].prevout) + return error("CheckProofOfStake(): block claimed PoS prevout doesn't match coinstakeTx (%s != %s)", body.coinstakePrevout.ToString(), block.vtx[1]->vin[0].prevout.ToString()); + + // check staker's pubKeyHash + { + std::vector addressRet; + txnouttype typeRet; + int nRet; + if (!ExtractDestinations(block.vtx[1]->vout[1].scriptPubKey, typeRet, addressRet, nRet)) + return error("CheckProofOfStake(): coinstakeTx scriptPubKey must be P2PKH"); + if (!(typeRet == txnouttype::TX_PUBKEYHASH && addressRet.size() == 1)) + return error("CheckProofOfStake(): coinstakeTx scriptPubKey must be P2PKH"); + + CKeyID keyID(boost::get(addressRet[0])); + if (keyID != body.pubKeyHash) + return error("CheckProofOfStake(): coinstakeTx scriptPubKey and block pubKeyHash mismatch"); + } + + // check staker's coin + { + const Coin& stakeCoin = view.AccessCoin(body.coinstakePrevout); + if (stakeCoin.IsSpent()) + return error("CheckProofOfStake : Could not find previous transaction for PoS %s\n", + body.coinstakePrevout.hash.ToString()); + if ((pindexPrev->nHeight - stakeCoin.nHeight) < params.pos.coinstakeMaturity) + return error("CheckProofOfStake(): coinstakeTx input must have at least 100 confirmations"); + + if (body.coinstakeAmount != stakeCoin.out.nValue) + return error("CheckProofOfStake(): coinstakeTx amount and block coinstakeAmount mismatch"); + // it's vital for security that we use the same scriptPubKey + if (block.vtx[1]->vout[1].scriptPubKey != stakeCoin.out.scriptPubKey) + return error("CheckProofOfStake(): coinstakeTx scriptPubKey and prev. scriptPubKey mismatch"); + } + + const int64_t coinstakeTime = (int64_t) block.GetBlockTime(); + + // checking PoS kernel is faster, so check it first + if (!CheckKernelHash(block.stakeModifier, block.nBits, coinstakeTime, body.coinstakeAmount, body.coinstakePrevout, + params).hashOk) { + return false; + } + return CheckHeaderSignature(block); + } + + uint32_t GetNextTargetRequired(const CBlockIndex* pindexLast, const CBlockHeader* pblock, + const Consensus::Params& params) { + return ::GetNextWorkRequired(pindexLast, pblock, params); + } + +} diff --git a/src/pos.h b/src/pos.h new file mode 100644 index 0000000000..77b549798f --- /dev/null +++ b/src/pos.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +class CBlock; + +class CBlockIndex; + +class CBlockHeader; + +class CTransaction; + +class COutPoint; + +class CCoinsViewCache; + +namespace pos { + +/// Check PoS signatures (PoS block hashes are signed with privkey of first coinstake out pubkey) + bool CheckHeaderSignature(const CBlockHeader& block); + +/// Check kernel hash target and coinstake signature + bool CheckProofOfStake_headerOnly(const CBlockHeader& block, const Consensus::Params& params); + +/// Check kernel hash target and coinstake signature. Check that block coinstakeTx matches header + bool CheckProofOfStake(const CBlockIndex* pindexPrev, const CBlock& block, CCoinsViewCache& view, + const Consensus::Params& params); + +/// @return nBits + uint32_t GetNextTargetRequired(const CBlockIndex* pindexLast, const CBlockHeader* pblock, + const Consensus::Params& params); + +} + diff --git a/src/primitives/block.cpp b/src/primitives/block.cpp index 60c7c2d160..0e80f12554 100644 --- a/src/primitives/block.cpp +++ b/src/primitives/block.cpp @@ -14,6 +14,11 @@ uint256 CBlockHeader::GetHash() const return SerializeHash(*this); } +uint256 CBlockHeader::GetHashToSign() const +{ + return SerializeHash(*this, SER_GETSIGNHASH); +} + std::string CBlock::ToString() const { std::stringstream s; diff --git a/src/primitives/block.h b/src/primitives/block.h index 750d42efbc..8023b5a0fa 100644 --- a/src/primitives/block.h +++ b/src/primitives/block.h @@ -9,6 +9,9 @@ #include #include #include +#include + +#include /** Nodes collect new transactions into a block, hash them into a hash tree, * and scan through nonce values to make the block's hash satisfy proof-of-work @@ -20,6 +23,30 @@ class CBlockHeader { public: + struct PoS { + COutPoint coinstakePrevout; + + CKeyID pubKeyHash; // only for sanity checks + + CAmount coinstakeAmount; // only for sanity checks + + // PoS: block signature - signed by staker's privkey + std::vector sig; + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) { + READWRITE(coinstakePrevout); + READWRITE(pubKeyHash); + READWRITE(coinstakeAmount); + bool hashToSignAction = s.GetType() & SER_GETSIGNHASH; + if (!hashToSignAction) { + READWRITE(sig); + } + } + }; + // header int32_t nVersion; uint256 hashPrevBlock; @@ -28,6 +55,9 @@ class CBlockHeader uint32_t nBits; uint32_t nNonce; + uint256 stakeModifier; // only for sanity checks + boost::optional proofOfStakeBody; + CBlockHeader() { SetNull(); @@ -60,6 +90,13 @@ class CBlockHeader return (nBits == 0); } + bool IsProofOfStake() const + { + return (bool) proofOfStakeBody; + } + + uint256 GetHashToSign() const; + uint256 GetHash() const; int64_t GetBlockTime() const @@ -104,6 +141,16 @@ class CBlock : public CBlockHeader fChecked = false; } + bool HasCoinstakeTx() const + { + return vtx.size() > 1 && vtx[1]->IsCoinStake(); + } + + bool IsCompleteProofOfStake() const + { + return IsProofOfStake() && HasCoinstakeTx(); + } + CBlockHeader GetBlockHeader() const { CBlockHeader block; diff --git a/src/primitives/transaction.h b/src/primitives/transaction.h index aad991e2f1..7eabb7b253 100644 --- a/src/primitives/transaction.h +++ b/src/primitives/transaction.h @@ -162,6 +162,10 @@ class CTxOut return (nValue == -1); } + bool IsEmpty() const { + return (nValue == 0 && scriptPubKey.empty()); + } + friend bool operator==(const CTxOut& a, const CTxOut& b) { return (a.nValue == b.nValue && @@ -339,6 +343,12 @@ class CTransaction return (vin.size() == 1 && vin[0].prevout.IsNull()); } + bool IsCoinStake() const + { + // PoS: the coin stake transaction is marked with the first output empty + return (vin.size() > 0 && (!vin[0].prevout.IsNull()) && vout.size() >= 2 && vout[0].IsEmpty()); + } + friend bool operator==(const CTransaction& a, const CTransaction& b) { return a.hash == b.hash; @@ -403,6 +413,14 @@ struct CMutableTransaction } return false; } + + bool IsCoinBase() const { + return (vin.size() == 1 && vin[0].prevout.IsNull()); + } + bool IsCoinStake() const { + // PoS: the coin stake transaction is marked with the first output empty + return (vin.size() > 0 && (!vin[0].prevout.IsNull()) && vout.size() >= 2 && vout[0].IsEmpty()); + } }; typedef std::shared_ptr CTransactionRef; diff --git a/src/serialize.h b/src/serialize.h index a38d76fc18..a74f2ffd8a 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -175,6 +175,7 @@ enum SER_NETWORK = (1 << 0), SER_DISK = (1 << 1), SER_GETHASH = (1 << 2), + SER_GETSIGNHASH = (1 << 3), }; //! Convert the reference base type to X, without changing constness or reference type.