diff --git a/src/Networks/x42/x42/Networks/Consensus/x42Consensus.cs b/src/Networks/x42/x42/Networks/Consensus/x42Consensus.cs index 969a6164a..8aecfed5d 100644 --- a/src/Networks/x42/x42/Networks/Consensus/x42Consensus.cs +++ b/src/Networks/x42/x42/Networks/Consensus/x42Consensus.cs @@ -154,6 +154,8 @@ public class x42Consensus : IConsensus /// public Money LastProofOfStakeRewardHeight { get; } + public Money MinOpReturnFee { get; } + public x42Consensus( ConsensusFactory consensusFactory, ConsensusOptions consensusOptions, @@ -188,6 +190,7 @@ public x42Consensus( Money proofOfStakeRewardAfterSubsidyLimit, long subsidyLimit, Money lastProofOfStakeRewardHeight, + Money minOpReturnFee, bool posEmptyCoinbase, uint proofOfStakeTimestampMask ) @@ -225,6 +228,7 @@ uint proofOfStakeTimestampMask this.ProofOfStakeRewardAfterSubsidyLimit = proofOfStakeRewardAfterSubsidyLimit; this.SubsidyLimit = subsidyLimit; this.LastProofOfStakeRewardHeight = lastProofOfStakeRewardHeight; + this.MinOpReturnFee = minOpReturnFee; this.ConsensusRules = new ConsensusRules(); this.MempoolRules = new List(); this.PosEmptyCoinbase = posEmptyCoinbase; diff --git a/src/Networks/x42/x42/Networks/Consensus/x42ConsensusErrors.cs b/src/Networks/x42/x42/Networks/Consensus/x42ConsensusErrors.cs new file mode 100644 index 000000000..721ac0dc4 --- /dev/null +++ b/src/Networks/x42/x42/Networks/Consensus/x42ConsensusErrors.cs @@ -0,0 +1,9 @@ +using Blockcore.Consensus; + +namespace x42.Networks.Consensus +{ + public static class x42ConsensusErrors + { + public static ConsensusError InsufficientOpReturnFee => new ConsensusError("op-return-fee-insufficient", "The OP_RETURN fee is insufficient."); + } +} \ No newline at end of file diff --git a/src/Networks/x42/x42/Networks/Consensus/x42PosBlockDefinition.cs b/src/Networks/x42/x42/Networks/Consensus/x42PosBlockDefinition.cs deleted file mode 100644 index 3e7e24ac5..000000000 --- a/src/Networks/x42/x42/Networks/Consensus/x42PosBlockDefinition.cs +++ /dev/null @@ -1,330 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Blockcore.Base.Deployments; -using Blockcore.Consensus; -using Blockcore.Features.MemoryPool; -using Blockcore.Features.MemoryPool.Interfaces; -using Blockcore.Features.Miner; -using Blockcore.Features.Miner.Comparers; -using Blockcore.Mining; -using Blockcore.Utilities; -using Microsoft.Extensions.Logging; -using NBitcoin; - -namespace x42.Networks.Consensus -{ - public class x42BlockDefinition : BlockDefinition - { - /// Instance logger. - private readonly ILogger logger; - - protected x42BlockDefinition( - IConsensusManager consensusManager, - IDateTimeProvider dateTimeProvider, - ILoggerFactory loggerFactory, - ITxMempool mempool, - MempoolSchedulerLock mempoolLock, - MinerSettings minerSettings, - Network network, - NodeDeployments nodeDeployments) : base(consensusManager, dateTimeProvider, loggerFactory, mempool, mempoolLock, minerSettings, network, nodeDeployments) - { - this.logger = loggerFactory.CreateLogger(this.GetType().FullName); - } - - public override void AddToBlock(TxMempoolEntry mempoolEntry) - { - base.AddTransactionToBlock(mempoolEntry.Transaction); - base.UpdateBlockStatistics(mempoolEntry); - base.UpdateTotalFees(mempoolEntry.Fee); - } - - public override BlockTemplate Build(ChainedHeader chainTip, Script scriptPubKey) - { - base.OnBuild(chainTip, scriptPubKey); - - base.coinbase.Outputs[0].ScriptPubKey = new Script(); - base.coinbase.Outputs[0].Value = Money.Zero; - - return base.BlockTemplate; - } - - public override void UpdateHeaders() - { - return; - } - - public static bool IsOpReturn(byte[] bytes) - { - return bytes.Length > 0 && bytes[0] == (byte)OpcodeType.OP_RETURN; - } - - protected override void AddTransactions(out int nPackagesSelected, out int nDescendantsUpdated) - { - nPackagesSelected = 0; - nDescendantsUpdated = 0; - - // mapModifiedTx will store sorted packages after they are modified - // because some of their txs are already in the block. - var mapModifiedTx = new Dictionary(); - - //var mapModifiedTxRes = this.mempoolScheduler.ReadAsync(() => mempool.MapTx.Values).GetAwaiter().GetResult(); - // mapModifiedTxRes.Select(s => new TxMemPoolModifiedEntry(s)).OrderBy(o => o, new CompareModifiedEntry()); - - // Keep track of entries that failed inclusion, to avoid duplicate work. - var failedTx = new TxMempool.SetEntries(); - - // Start by adding all descendants of previously added txs to mapModifiedTx - // and modifying them for their already included ancestors. - UpdatePackagesForAdded(this.inBlock, mapModifiedTx); - - List ancestorScoreList = this.MempoolLock.ReadAsync(() => this.Mempool.MapTx.AncestorScore).ConfigureAwait(false).GetAwaiter().GetResult().ToList(); - - TxMempoolEntry iter; - - int nConsecutiveFailed = 0; - while (ancestorScoreList.Any() || mapModifiedTx.Any()) - { - TxMempoolEntry mi = ancestorScoreList.FirstOrDefault(); - if (mi != null) - { - // Skip entries in mapTx that are already in a block or are present - // in mapModifiedTx (which implies that the mapTx ancestor state is - // stale due to ancestor inclusion in the block). - // Also skip transactions that we've already failed to add. This can happen if - // we consider a transaction in mapModifiedTx and it fails: we can then - // potentially consider it again while walking mapTx. It's currently - // guaranteed to fail again, but as a belt-and-suspenders check we put it in - // failedTx and avoid re-evaluation, since the re-evaluation would be using - // cached size/sigops/fee values that are not actually correct. - - // First try to find a new transaction in mapTx to evaluate. - if (mapModifiedTx.ContainsKey(mi.TransactionHash) || this.inBlock.Contains(mi) || failedTx.Contains(mi)) - { - ancestorScoreList.Remove(mi); - continue; - } - } - - // Now that mi is not stale, determine which transaction to evaluate: - // the next entry from mapTx, or the best from mapModifiedTx? - bool fUsingModified = false; - TxMemPoolModifiedEntry modit; - var compare = new CompareModifiedEntry(); - if (mi == null) - { - modit = mapModifiedTx.Values.OrderBy(o => o, compare).First(); - iter = modit.MempoolEntry; - fUsingModified = true; - } - else - { - // Try to compare the mapTx entry to the mapModifiedTx entry - iter = mi; - - modit = mapModifiedTx.Values.OrderBy(o => o, compare).FirstOrDefault(); - if ((modit != null) && (compare.Compare(modit, new TxMemPoolModifiedEntry(iter)) < 0)) - { - // The best entry in mapModifiedTx has higher score - // than the one from mapTx.. - // Switch which transaction (package) to consider. - - iter = modit.MempoolEntry; - fUsingModified = true; - } - else - { - // Either no entry in mapModifiedTx, or it's worse than mapTx. - // Increment mi for the next loop iteration. - ancestorScoreList.Remove(iter); - } - } - - // We skip mapTx entries that are inBlock, and mapModifiedTx shouldn't - // contain anything that is inBlock. - Guard.Assert(!this.inBlock.Contains(iter)); - - long packageSize = iter.SizeWithAncestors; - Money packageFees = iter.ModFeesWithAncestors; - long packageSigOpsCost = iter.SigOpCostWithAncestors; - if (fUsingModified) - { - packageSize = modit.SizeWithAncestors; - packageFees = modit.ModFeesWithAncestors; - packageSigOpsCost = modit.SigOpCostWithAncestors; - } - - int opReturnCount = iter.Transaction.Outputs.Select(o => o.ScriptPubKey.ToBytes(true)).Count(b => IsOpReturn(b)); - - // When there are zero fee's we want to still include the transaction. - // If there is OP_RETURN data, we will want to make sure there is a fee. - if (this.Network.MinTxFee > Money.Zero || opReturnCount > 0) - { - if (packageFees < this.BlockMinFeeRate.GetFee((int)packageSize)) - { - // Everything else we might consider has a lower fee rate - return; - } - } - - if (!this.TestPackage(iter, packageSize, packageSigOpsCost)) - { - if (fUsingModified) - { - // Since we always look at the best entry in mapModifiedTx, - // we must erase failed entries so that we can consider the - // next best entry on the next loop iteration - mapModifiedTx.Remove(modit.MempoolEntry.TransactionHash); - failedTx.Add(iter); - } - - nConsecutiveFailed++; - - if ((nConsecutiveFailed > MaxConsecutiveAddTransactionFailures) && (this.BlockWeight > this.Options.BlockMaxWeight - 4000)) - { - // Give up if we're close to full and haven't succeeded in a while - break; - } - continue; - } - - var ancestors = new TxMempool.SetEntries(); - long nNoLimit = long.MaxValue; - string dummy; - - this.MempoolLock.ReadAsync(() => this.Mempool.CalculateMemPoolAncestors(iter, ancestors, nNoLimit, nNoLimit, nNoLimit, nNoLimit, out dummy, false)).ConfigureAwait(false).GetAwaiter().GetResult(); - - this.OnlyUnconfirmed(ancestors); - ancestors.Add(iter); - - // Test if all tx's are Final. - if (!this.TestPackageTransactions(ancestors)) - { - if (fUsingModified) - { - mapModifiedTx.Remove(modit.MempoolEntry.TransactionHash); - failedTx.Add(iter); - } - continue; - } - - // This transaction will make it in; reset the failed counter. - nConsecutiveFailed = 0; - - // Package can be added. Sort the entries in a valid order. - // Sort package by ancestor count - // If a transaction A depends on transaction B, then A's ancestor count - // must be greater than B's. So this is sufficient to validly order the - // transactions for block inclusion. - List sortedEntries = ancestors.ToList().OrderBy(o => o, new CompareTxIterByAncestorCount()).ToList(); - foreach (TxMempoolEntry sortedEntry in sortedEntries) - { - this.AddToBlock(sortedEntry); - // Erase from the modified set, if present - mapModifiedTx.Remove(sortedEntry.TransactionHash); - } - - nPackagesSelected++; - - // Update transactions that depend on each of these - nDescendantsUpdated += this.UpdatePackagesForAdded(ancestors, mapModifiedTx); - } - } - - /// - /// Add descendants of given transactions to mapModifiedTx with ancestor - /// state updated assuming given transactions are inBlock. Returns number - /// of updated descendants. - /// - private int UpdatePackagesForAdded(TxMempool.SetEntries alreadyAdded, Dictionary mapModifiedTx) - { - int descendantsUpdated = 0; - - foreach (TxMempoolEntry addedEntry in alreadyAdded) - { - var setEntries = new TxMempool.SetEntries(); - - this.MempoolLock.ReadAsync(() => - { - if (!this.Mempool.MapTx.ContainsKey(addedEntry.TransactionHash)) - { - this.logger.LogWarning("{0} is not present in {1} any longer, skipping.", addedEntry.TransactionHash, nameof(this.Mempool.MapTx)); - return; - } - - this.Mempool.CalculateDescendants(addedEntry, setEntries); - - }).GetAwaiter().GetResult(); - - foreach (TxMempoolEntry desc in setEntries) - { - if (alreadyAdded.Contains(desc)) - continue; - - descendantsUpdated++; - TxMemPoolModifiedEntry modEntry; - if (!mapModifiedTx.TryGetValue(desc.TransactionHash, out modEntry)) - { - modEntry = new TxMemPoolModifiedEntry(desc); - mapModifiedTx.Add(desc.TransactionHash, modEntry); - this.logger.LogDebug("Added transaction '{0}' to the block template because it's a required ancestor for '{1}'.", desc.TransactionHash, addedEntry.TransactionHash); - } - - modEntry.SizeWithAncestors -= addedEntry.GetTxSize(); - modEntry.ModFeesWithAncestors -= addedEntry.ModifiedFee; - modEntry.SigOpCostWithAncestors -= addedEntry.SigOpCost; - } - } - - return descendantsUpdated; - } - - /// - /// Remove confirmed entries from given set. - /// - private void OnlyUnconfirmed(TxMempool.SetEntries testSet) - { - foreach (TxMempoolEntry setEntry in testSet.ToList()) - { - // Only test txs not already in the block - if (this.inBlock.Contains(setEntry)) - { - testSet.Remove(setEntry); - } - } - } - - /// - /// Perform transaction-level checks before adding to block. - /// - /// - /// Transaction finality (locktime). - /// Premature witness (in case segwit transactions are added to mempool before segwit activation). - /// serialized size (in case -blockmaxsize is in use). - /// - /// - /// - private bool TestPackageTransactions(TxMempool.SetEntries package) - { - foreach (TxMempoolEntry it in package) - { - if (!it.Transaction.IsFinal(Utils.UnixTimeToDateTime(this.LockTimeCutoff), this.height)) - return false; - - if (!this.IncludeWitness && it.Transaction.HasWitness) - return false; - - if (this.NeedSizeAccounting) - { - long nPotentialBlockSize = this.BlockSize; // only used with needSizeAccounting - int nTxSize = it.Transaction.GetSerializedSize(); - if (nPotentialBlockSize + nTxSize >= this.Options.BlockMaxSize) - return false; - - nPotentialBlockSize += nTxSize; - } - } - - return true; - } - } -} \ No newline at end of file diff --git a/src/Networks/x42/x42/Networks/Rules/x42OpReturnFeeMempoolRule.cs b/src/Networks/x42/x42/Networks/Rules/x42OpReturnFeeMempoolRule.cs new file mode 100644 index 000000000..1d4f722fe --- /dev/null +++ b/src/Networks/x42/x42/Networks/Rules/x42OpReturnFeeMempoolRule.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Linq; +using Blockcore.Features.MemoryPool; +using Blockcore.Features.MemoryPool.Interfaces; +using Blockcore.Features.MemoryPool.Rules; +using Microsoft.Extensions.Logging; +using NBitcoin; + +namespace x42.Networks.Consensus.Rules +{ + /// + /// Verify the OP_RETURN fee. + /// + public class x42OpReturnFeeMempoolRule : CheckFeeMempoolRule + { + public x42OpReturnFeeMempoolRule(Network network, + ITxMempool mempool, + MempoolSettings mempoolSettings, + ChainIndexer chainIndexer, + ILoggerFactory loggerFactory) : base(network, mempool, mempoolSettings, chainIndexer, loggerFactory) + { + } + + public override void CheckTransaction(MempoolValidationContext context) + { + if (context.Transaction.IsCoinBase) + return; + + List opReturns = context.Transaction.Outputs.Select(o => o.ScriptPubKey.ToBytes(true)).Where(b => IsOpReturn(b)).ToList(); + Money transactionFees = context.Fees; + FeeRate OpReturnFeeRate = new FeeRate(((x42Consensus)this.network.Consensus).MinOpReturnFee); + + // If there is OP_RETURN data, we will want to make sure the fee is correct. + if (opReturns.Count() > 0) + { + var opReturnSize = opReturns.Sum(r => r.Length); + if (transactionFees < OpReturnFeeRate.GetFee(opReturnSize)) + { + this.logger.LogTrace($"(-)[FAIL_{nameof(x42OpReturnFeeMempoolRule)}]".ToUpperInvariant()); + x42ConsensusErrors.InsufficientOpReturnFee.Throw(); + } + } + + base.CheckTransaction(context); + } + + public static bool IsOpReturn(byte[] bytes) + { + return bytes.Length > 0 && bytes[0] == (byte)OpcodeType.OP_RETURN; + } + } +} \ No newline at end of file diff --git a/src/Networks/x42/x42/Networks/x42Main.cs b/src/Networks/x42/x42/Networks/x42Main.cs index b99446ea5..0fb703573 100644 --- a/src/Networks/x42/x42/Networks/x42Main.cs +++ b/src/Networks/x42/x42/Networks/x42Main.cs @@ -15,6 +15,7 @@ using x42.Networks.Setup; using x42.Networks.Deployments; using NBitcoin.Protocol; +using x42.Networks.Consensus.Rules; namespace x42.Networks { @@ -129,6 +130,7 @@ public x42Main() subsidyLimit: setup.SubsidyLimit, proofOfStakeRewardAfterSubsidyLimit: setup.ProofOfStakeRewardAfterSubsidyLimit, lastProofOfStakeRewardHeight: setup.LastProofOfStakeRewardHeight, + minOpReturnFee: Money.Coins(0.02m).Satoshi, proofOfStakeTimestampMask: setup.ProofOfStakeTimestampMask, posEmptyCoinbase: x42Setup.Instance.IsPoSv3() ) @@ -227,7 +229,7 @@ protected void RegisterMempoolRules(IConsensus consensus) typeof(CheckCoinViewMempoolRule), typeof(CreateMempoolEntryMempoolRule), typeof(CheckSigOpsMempoolRule), - typeof(CheckFeeMempoolRule), + typeof(x42OpReturnFeeMempoolRule), typeof(CheckRateLimitMempoolRule), typeof(CheckAncestorsMempoolRule), typeof(CheckReplacementMempoolRule), diff --git a/src/Networks/x42/x42/Networks/x42RegTest.cs b/src/Networks/x42/x42/Networks/x42RegTest.cs index 350425995..9fa8c72ab 100644 --- a/src/Networks/x42/x42/Networks/x42RegTest.cs +++ b/src/Networks/x42/x42/Networks/x42RegTest.cs @@ -105,6 +105,7 @@ public x42RegTest() subsidyLimit: setup.SubsidyLimit, proofOfStakeRewardAfterSubsidyLimit: setup.ProofOfStakeRewardAfterSubsidyLimit, lastProofOfStakeRewardHeight: setup.LastProofOfStakeRewardHeight, + minOpReturnFee: Money.Coins(0.02m).Satoshi, proofOfStakeTimestampMask: setup.ProofOfStakeTimestampMask, posEmptyCoinbase: x42Setup.Instance.IsPoSv3() ) diff --git a/src/Networks/x42/x42/Networks/x42Test.cs b/src/Networks/x42/x42/Networks/x42Test.cs index c5889c49f..6aa6e8b1f 100644 --- a/src/Networks/x42/x42/Networks/x42Test.cs +++ b/src/Networks/x42/x42/Networks/x42Test.cs @@ -127,6 +127,7 @@ public x42Test() proofOfStakeRewardAfterSubsidyLimit: setup.ProofOfStakeRewardAfterSubsidyLimit, lastProofOfStakeRewardHeight: setup.LastProofOfStakeRewardHeight, proofOfStakeTimestampMask: setup.ProofOfStakeTimestampMask, + minOpReturnFee: Money.Coins(0.02m).Satoshi, posEmptyCoinbase: x42Setup.Instance.IsPoSv3() ) {