Skip to content

Commit

Permalink
Merge pull request stratisproject#39 from quantumagi/coldstakingrule2
Browse files Browse the repository at this point in the history
New Validation Rule For Coinstake Transaction 2
  • Loading branch information
noescape00 authored Aug 24, 2018
2 parents d268311 + 97d5284 commit abbeb50
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 0 deletions.
4 changes: 4 additions & 0 deletions src/Stratis.Bitcoin.Features.Consensus/ConsensusErrors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,9 @@ public static class ConsensusErrors
public static readonly ConsensusError ProofOfWorkTooHigh = new ConsensusError("proof-of-work-too-heigh", "proof of work too high");

public static readonly ConsensusError CheckpointViolation = new ConsensusError("checkpoint-violation", "block header hash does not match the checkpointed value");

public static readonly ConsensusError BadColdstakeAmount = new ConsensusError("bad-coldstake-amount", "coldstake is negative");
public static readonly ConsensusError BadColdstakeInputs = new ConsensusError("bad-coldstake-inputs", "coldstake inputs contain mismatching scriptpubkeys");
public static readonly ConsensusError BadColdstakeOutputs = new ConsensusError("bad-coldstake-outputs", "coldstake outputs contain unexpected scriptpubkeys");
}
}
3 changes: 3 additions & 0 deletions src/Stratis.Bitcoin.Features.Consensus/ConsensusFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,9 @@ public void RegisterRules(IConsensus consensus)
new LoadCoinviewRule(),
new TransactionDuplicationActivationRule(), // implements BIP30
new PosCoinviewRule(), // implements BIP68, MaxSigOps and BlockReward calculation
// Place the PosColdStakingRule after the PosCoinviewRule to ensure that all input scripts have been evaluated
// and that the "IsColdCoinStake" flag would have been set by the OP_CHECKCOLDSTAKEVERIFY opcode if applicable.
new PosColdStakingRule(),
new SaveCoinviewRule()
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using NBitcoin;
using Stratis.Bitcoin.Base.Deployments;
using Stratis.Bitcoin.Consensus.Rules;
namespace Stratis.Bitcoin.Features.Consensus.Rules.CommonRules
{
/// <summary>
/// This rule performs further coldstaking transaction validation when cold staking balances are spent with a
/// cold staking "hotPubKey" inside of a cold staking transaction. The cold staking "hotPubKey" is the pubKey
/// that allows spending only to the same address.
/// </summary>
/// <remarks><para>
/// This code will perform further validation for transactions that spend the new scriptPubKey containing the
/// new <see cref="OpcodeType.OP_CHECKCOLDSTAKEVERIFY"/> opcode using a hotPubKeyHash inside of a coinstake
/// transaction. Those are the conditions under which the <see cref="PosTransaction.IsColdCoinStake"/> flag will
/// be set and it is therefore the flag we use to determine if these rules should be applied. Due to a check
/// being done inside of the implementation of the <see cref="OpcodeType.OP_CHECKCOLDSTAKEVERIFY"/> opcode this
/// flag can only be set for coinstake transactions after the opcode implementation has been activated (at a
/// specified block height). The opcode activation flag is <see cref="ScriptVerify.CheckColdStakeVerify"/> and
/// it is set in <see cref="DeploymentFlags.ScriptFlags"/> when the block height is greater than or equal to <see
/// cref="PosConsensusOptions.ColdStakingActivationHeight"/>.
/// </para><para>
/// The following conditions are enforced for cold staking transactions. This rule implements all but the first one:
/// <list type="number">
/// <item>Check if the transaction spending an output, which contains this instruction, is a coinstake transaction.
/// If it is not, the script fails. This has already been checked within the opcode implementation before setting
/// <see cref="PosTransaction.IsColdCoinStake"/> so it will not be checked here.</item>
/// <item>Check that ScriptPubKeys of all inputs of this transaction are the same. If they are not, the script
/// fails.</item>
/// <item>Check that ScriptPubKeys of all outputs of this transaction, except for the marker output (a special
/// first output of each coinstake transaction) and the pubkey output (an optional special second output that
/// contains public key in coinstake transaction), are the same as ScriptPubKeys of the inputs. If they are not,
/// the script fails.</item>
/// <item>Check that the sum of values of all inputs is smaller or equal to the sum of values of all outputs. If
/// this does not hold, the script fails.</item>
/// </list></para></remarks>
public class PosColdStakingRule : UtxoStoreConsensusRule
{
/// <inheritdoc />
/// <exception cref="ConsensusErrors.BadColdstakeInputs">Thrown if the input scriptPubKeys mismatch.</exception>
/// <exception cref="ConsensusErrors.BadColdstakeOutputs">Thrown if the output scriptPubKeys mismatch.</exception>
/// <exception cref="ConsensusErrors.BadColdstakeAmount">Thrown if the total input is smaller or equal than the sum of outputs.</exception>
public override Task RunAsync(RuleContext context)
{
this.Logger.LogTrace("()");

// Take the coinstake transaction from the block and check if the flag ("IsColdCoinStake") is set.
// The flag will only be set in the OP_CHECKCOLDSTAKEVERIFY if ScriptFlags in DeploymentFlags has "CheckColdStakeVerify" set.
Block block = context.ValidationContext.BlockToValidate;
if ((block.Transactions.Count < 2) || !((block.Transactions[1] is PosTransaction coinstakeTransaction) && coinstakeTransaction.IsColdCoinStake))
{
// If it is not a cold coin stake transaction then this rule is not required.
this.Logger.LogTrace("(-)[SKIP_COLDSTAKE_RULE]");
return Task.CompletedTask;
}

var utxoRuleContext = context as UtxoRuleContext;
UnspentOutputSet view = utxoRuleContext.UnspentOutputSet;

// Verify that GetOutputFor returns non-null for all inputs.
if (!view.HaveInputs(coinstakeTransaction))
{
this.Logger.LogTrace("(-)[COLDSTAKE_INPUTS_WITHOUT_OUTPUTS]");
ConsensusErrors.BadColdstakeInputs.Throw();
}

// Check that ScriptPubKeys of all inputs of this transaction are the same. If they are not, the script fails.
// Due to this being a coinstake transaction we know if will have at least one input.
Script scriptPubKey = view.GetOutputFor(coinstakeTransaction.Inputs[0]).ScriptPubKey;
for (int i = 1; i < coinstakeTransaction.Inputs.Count; i++)
{
if (scriptPubKey != view.GetOutputFor(coinstakeTransaction.Inputs[i]).ScriptPubKey)
{
this.Logger.LogTrace("(-)[BAD_COLDSTAKE_INPUTS]");
ConsensusErrors.BadColdstakeInputs.Throw();
}
}

// Check that the second output is a special output for presenting the public key with an OP_RETURN and that
// the output value is zero. Checking for the OP_RETURN ensures that the PosBlockSignatureRule won't match the
// PayToPubKey template. This will ensure that an attacker won't use a PayToPubKey output here to spend our
// cold staking balance (using the hot wallet key) to an address other then our special scriptpubkey.
if ((coinstakeTransaction.Outputs[1].ScriptPubKey.ToOps().FirstOrDefault()?.Code != OpcodeType.OP_RETURN) ||
(coinstakeTransaction.Outputs[1].Value != Money.Zero))
{
this.Logger.LogTrace("(-)[MISSING_COLDSTAKE_PUBKEY_OUTPUT]");
ConsensusErrors.BadColdstakeOutputs.Throw();
}

// Check that ScriptPubKeys of all outputs of this transaction, except for the marker output (a special first
// output of each coinstake transaction) and the pubkey output (an optional special second output that contains
// public key in coinstake transaction), are the same as ScriptPubKeys of the inputs. If they are not, the script fails.
for (int i = 2; i < coinstakeTransaction.Outputs.Count; i++)
{
if (scriptPubKey != coinstakeTransaction.Outputs[i].ScriptPubKey)
{
this.Logger.LogTrace("(-)[BAD_COLDSTAKE_OUTPUTS]");
ConsensusErrors.BadColdstakeOutputs.Throw();
}
}

// Check that the sum of values of all inputs is smaller or equal to the sum of values of all outputs. If this does
// not hold, the script fails.
if (view.GetValueIn(coinstakeTransaction) > coinstakeTransaction.TotalOut)
{
this.Logger.LogTrace("(-)[COLDSTAKE_INPUTS_EXCEED_OUTPUTS]");
ConsensusErrors.BadColdstakeAmount.Throw();
}

this.Logger.LogTrace("(-)");

return Task.CompletedTask;
}
}
}

0 comments on commit abbeb50

Please sign in to comment.