/// <summary> /// Checks that the stake kernel hash satisfies the target difficulty. /// </summary> /// <param name="context">Staking context.</param> /// <param name="headerBits">Chained block's header bits, which define the difficulty target.</param> /// <param name="prevBlockStake">Information about previous staked block.</param> /// <param name="stakingCoins">Coins that participate in staking.</param> /// <param name="prevout">Information about transaction id and index.</param> /// <param name="transactionTime">Transaction time.</param> /// <remarks> /// Coinstake must meet hash target according to the protocol: /// kernel (input 0) must meet the formula /// <c>hash(stakeModifierV2 + stakingCoins.Time + prevout.Hash + prevout.N + transactionTime) < target * weight</c>. /// This ensures that the chance of getting a coinstake is proportional to the amount of coins one owns. /// <para> /// The reason this hash is chosen is the following: /// <list type="number"> /// <item><paramref name="prevBlockStake.StakeModifierV2"/>: Scrambles computation to make it very difficult to precompute future proof-of-stake.</item> /// <item><paramref name="stakingCoins.Time"/>: Time of the coinstake UTXO. Slightly scrambles computation.</item> /// <item><paramref name="prevout.Hash"/> Hash of stakingCoins UTXO, to reduce the chance of nodes generating coinstake at the same time.</item> /// <item><paramref name="prevout.N"/>: Output number of stakingCoins UTXO, to reduce the chance of nodes generating coinstake at the same time.</item> /// <item><paramref name="transactionTime"/>: Timestamp of the coinstake transaction.</item> /// </list> /// Block or transaction tx hash should not be used here as they can be generated in vast /// quantities so as to generate blocks faster, degrading the system back into a proof-of-work situation. /// </para> /// </remarks> /// <exception cref="ConsensusErrors.StakeTimeViolation">Thrown in case transaction time is lower than it's own UTXO timestamp.</exception> /// <exception cref="ConsensusErrors.StakeHashInvalidTarget">Thrown in case PoS hash doesn't meet target protocol.</exception> private void CheckStakeKernelHash(ContextStakeInformation context, uint headerBits, BlockStake prevBlockStake, UnspentOutputs stakingCoins, OutPoint prevout, uint transactionTime) { this.logger.LogTrace("({0}:{1:X},{2}.{3}:'{4}',{5}:'{6}/{7}',{8}:'{9}',{10}:{11})", nameof(headerBits), headerBits, nameof(prevBlockStake), nameof(prevBlockStake.HashProof), prevBlockStake.HashProof, nameof(stakingCoins), stakingCoins.TransactionId, stakingCoins.Height, nameof(prevout), prevout, nameof(transactionTime), transactionTime); if (transactionTime < stakingCoins.Time) { this.logger.LogTrace("Coinstake transaction timestamp {0} is lower than it's own UTXO timestamp {1}.", transactionTime, stakingCoins.Time); this.logger.LogTrace("(-)[BAD_STAKE_TIME]"); ConsensusErrors.StakeTimeViolation.Throw(); } // Base target. BigInteger target = new Target(headerBits).ToBigInteger(); // TODO: Investigate: // The POS protocol should probably put a limit on the max amount that can be staked // not a hard limit but a limit that allow any amount to be staked with a max weight value. // the max weight should not exceed the max uint256 array size (array size = 32). // Weighted target. long valueIn = stakingCoins.Outputs[prevout.N].Value.Satoshi; BigInteger weight = BigInteger.ValueOf(valueIn); BigInteger weightedTarget = target.Multiply(weight); context.TargetProofOfStake = ToUInt256(weightedTarget); this.logger.LogTrace("POS target is '{0}', weighted target for {1} coins is '{2}'.", ToUInt256(target), valueIn, context.TargetProofOfStake); uint256 stakeModifierV2 = prevBlockStake.StakeModifierV2; // Calculate hash. using (var ms = new MemoryStream()) { var serializer = new BitcoinStream(ms, true); serializer.ReadWrite(stakeModifierV2); serializer.ReadWrite(stakingCoins.Time); serializer.ReadWrite(prevout.Hash); serializer.ReadWrite(prevout.N); serializer.ReadWrite(transactionTime); context.HashProofOfStake = Hashes.Hash256(ms.ToArray()); } this.logger.LogTrace("Stake modifier V2 is '{0}', hash POS is '{1}'.", stakeModifierV2, context.HashProofOfStake); // Now check if proof-of-stake hash meets target protocol. var hashProofOfStakeTarget = new BigInteger(1, context.HashProofOfStake.ToBytes(false)); if (hashProofOfStakeTarget.CompareTo(weightedTarget) > 0) { this.logger.LogTrace("(-)[TARGET_MISSED]"); ConsensusErrors.StakeHashInvalidTarget.Throw(); } this.logger.LogTrace("(-)[OK]"); }
/// <inheritdoc/> public void CheckProofOfStake(ContextStakeInformation context, ChainedBlock prevChainedBlock, BlockStake prevBlockStake, Transaction transaction, uint headerBits) { this.logger.LogTrace("({0}:'{1}',{2}.{3}:'{4}',{5}:0x{6:X})", nameof(prevChainedBlock), prevChainedBlock.HashBlock, nameof(prevBlockStake), nameof(prevBlockStake.HashProof), prevBlockStake.HashProof, nameof(headerBits), headerBits); if (!transaction.IsCoinStake) { this.logger.LogTrace("(-)[NO_COINSTAKE]"); ConsensusErrors.NonCoinstake.Throw(); } TxIn txIn = transaction.Inputs[0]; // First try finding the previous transaction in database. FetchCoinsResponse coins = this.coinView.FetchCoinsAsync(new[] { txIn.PrevOut.Hash }).GetAwaiter().GetResult(); if ((coins == null) || (coins.UnspentOutputs.Length != 1)) { ConsensusErrors.ReadTxPrevFailed.Throw(); } UnspentOutputs prevUtxo = coins.UnspentOutputs[0]; if (prevUtxo == null) { this.logger.LogTrace("(-)[PREV_UTXO_IS_NULL]"); ConsensusErrors.ReadTxPrevFailed.Throw(); } // Verify signature. if (!this.VerifySignature(prevUtxo, transaction, 0, ScriptVerify.None)) { this.logger.LogTrace("(-)[BAD_SIGNATURE]"); ConsensusErrors.CoinstakeVerifySignatureFailed.Throw(); } // Min age requirement. if (this.IsConfirmedInNPrevBlocks(prevUtxo, prevChainedBlock, this.consensusOptions.StakeMinConfirmations - 1)) { this.logger.LogTrace("(-)[BAD_STAKE_DEPTH]"); ConsensusErrors.InvalidStakeDepth.Throw(); } this.CheckStakeKernelHash(context, headerBits, prevBlockStake, prevUtxo, txIn.PrevOut, transaction.Time); this.logger.LogTrace("(-)[OK]"); }
/// <inheritdoc/> public void CheckKernel(ContextStakeInformation context, ChainedBlock prevChainedBlock, uint headerBits, long transactionTime, OutPoint prevout) { this.logger.LogTrace("({0}:'{1}',{2}:0x{3:X},{4}:{5},{6}:'{7}.{8}')", nameof(prevChainedBlock), prevChainedBlock, nameof(headerBits), headerBits, nameof(transactionTime), transactionTime, nameof(prevout), prevout.Hash, prevout.N); FetchCoinsResponse coins = this.coinView.FetchCoinsAsync(new[] { prevout.Hash }).GetAwaiter().GetResult(); if ((coins == null) || (coins.UnspentOutputs.Length != 1)) { this.logger.LogTrace("(-)[READ_PREV_TX_FAILED]"); ConsensusErrors.ReadTxPrevFailed.Throw(); } ChainedBlock prevBlock = this.chain.GetBlock(coins.BlockHash); if (prevBlock == null) { this.logger.LogTrace("(-)[REORG]"); ConsensusErrors.ReadTxPrevFailed.Throw(); } UnspentOutputs prevUtxo = coins.UnspentOutputs[0]; if (this.IsConfirmedInNPrevBlocks(prevUtxo, prevChainedBlock, this.consensusOptions.StakeMinConfirmations - 1)) { this.logger.LogTrace("(-)[LOW_COIN_AGE]"); ConsensusErrors.InvalidStakeDepth.Throw(); } BlockStake prevBlockStake = this.stakeChain.Get(prevChainedBlock.HashBlock); if (prevBlockStake == null) { this.logger.LogTrace("(-)[BAD_STAKE_BLOCK]"); ConsensusErrors.BadStakeBlock.Throw(); } this.CheckStakeKernelHash(context, headerBits, prevBlockStake, prevUtxo, prevout, (uint)transactionTime); }