private List <ScriptCoin> GetRewardCoins(Transaction coinStake) { var coins = new List <ScriptCoin>(); // Identify any outputs paying the reward script a nonzero amount. TxOut[] rewardOutputs = coinStake.Outputs.Where(o => o.ScriptPubKey == StraxCoinstakeRule.CirrusRewardScript && o.Value != 0).ToArray(); // This shouldn't be the case but check anyway. if (rewardOutputs.Length != 0) { foreach (TxOut txOutput in rewardOutputs) { // The reward script is P2SH, so we need to inform the builder of the corresponding redeem script to enable it to be spent. var coin = ScriptCoin.Create(this.network, coinStake, txOutput, StraxCoinstakeRule.CirrusRewardScriptRedeem); coins.Add(coin); } } return(coins); }
public async Task AcceptToMemoryPool_WithSegWitValidTxns_IsSuccessfullAsync() { string dataDir = GetTestDirectoryPath(this); var miner = new BitcoinSecret(new Key(), Network.RegTest); ITestChainContext context = await TestChainFactory.CreateAsync(Network.RegTest, miner.PubKey.ScriptPubKey.WitHash.ScriptPubKey.Hash.ScriptPubKey, dataDir); IMempoolValidator validator = context.MempoolValidator; Assert.NotNull(validator); var bob = new BitcoinSecret(new Key(), Network.RegTest); // Fund Bob // 50 Coins come from first tx on chain - send bob 42 and change back to miner ScriptCoin witnessCoin = ScriptCoin.Create(Network.RegTest, context.SrcTxs[0].GetHash(), 0, context.SrcTxs[0].TotalOut, miner.PubKey.ScriptPubKey.WitHash.ScriptPubKey.Hash.ScriptPubKey, miner.PubKey.ScriptPubKey); var txBuilder = new TransactionBuilder(Network.RegTest); Transaction p2shOverp2wpkh = txBuilder .AddCoins(witnessCoin) .AddKeys(miner) .Send(bob, "42.00") .SendFees("0.001") .SetChange(miner) .BuildTransaction(true); Assert.True(txBuilder.Verify(p2shOverp2wpkh)); //check fully signed // remove witness data from tx Transaction noWitTx = p2shOverp2wpkh.WithOptions(TransactionOptions.None, Network.RegTest.Consensus.ConsensusFactory); Assert.Equal(p2shOverp2wpkh.GetHash(), noWitTx.GetHash()); Assert.True(noWitTx.GetSerializedSize() < p2shOverp2wpkh.GetSerializedSize()); Assert.True(txBuilder.Verify(p2shOverp2wpkh)); //check fully signed var state = new MempoolValidationState(false); Assert.True(await validator.AcceptToMemoryPool(state, p2shOverp2wpkh), $"Transaction: {nameof(p2shOverp2wpkh)} failed mempool validation."); }
public async Task AcceptToMemoryPool_WithP2SHValidTxns_IsSuccessfullAsync() { string dataDir = GetTestDirectoryPath(this); var miner = new BitcoinSecret(new Key(), Network.RegTest); ITestChainContext context = await TestChainFactory.CreateAsync(Network.RegTest, miner.PubKey.Hash.ScriptPubKey, dataDir); IMempoolValidator validator = context.MempoolValidator; Assert.NotNull(validator); var alice = new BitcoinSecret(new Key(), Network.RegTest); var bob = new BitcoinSecret(new Key(), Network.RegTest); var satoshi = new BitcoinSecret(new Key(), Network.RegTest); var nico = new BitcoinSecret(new Key(), Network.RegTest); // corp needs two out of three of alice, bob, nico Script corpMultiSig = PayToMultiSigTemplate .Instance .GenerateScriptPubKey(2, new[] { alice.PubKey, bob.PubKey, nico.PubKey }); // P2SH address for corp multi-sig BitcoinScriptAddress corpRedeemAddress = corpMultiSig.GetScriptAddress(Network.RegTest); // Fund corp // 50 Coins come from first tx on chain - send corp 42 and change back to miner var coin = new Coin(context.SrcTxs[0].GetHash(), 0, context.SrcTxs[0].TotalOut, miner.ScriptPubKey); var txBuilder = new TransactionBuilder(Network.RegTest); Transaction fundP2shTx = txBuilder .AddCoins(new List <Coin> { coin }) .AddKeys(miner) .Send(corpRedeemAddress, "42.00") .SendFees("0.001") .SetChange(miner.GetAddress()) .BuildTransaction(true); Assert.True(txBuilder.Verify(fundP2shTx)); //check fully signed var state = new MempoolValidationState(false); Assert.True(await validator.AcceptToMemoryPool(state, fundP2shTx), $"Transaction: {nameof(fundP2shTx)} failed mempool validation."); // AliceBobNico corp. send 20 to Satoshi Coin[] corpCoins = fundP2shTx.Outputs .Where(o => o.ScriptPubKey == corpRedeemAddress.ScriptPubKey) .Select(o => ScriptCoin.Create(Network.RegTest, new OutPoint(fundP2shTx.GetHash(), fundP2shTx.Outputs.IndexOf(o)), o, corpMultiSig)) .ToArray(); txBuilder = new TransactionBuilder(Network.RegTest); Transaction p2shSpendTx = txBuilder .AddCoins(corpCoins) .AddKeys(alice, bob) .Send(satoshi.GetAddress(), "20") .SendFees("0.001") .SetChange(corpRedeemAddress) .BuildTransaction(true); Assert.True(txBuilder.Verify(p2shSpendTx)); Assert.True(await validator.AcceptToMemoryPool(state, p2shSpendTx), $"Transaction: {nameof(p2shSpendTx)} failed mempool validation."); }
/// <summary> /// Creates a transaction to transfers funds from an old federation to a new federation. /// </summary> /// <param name="isSideChain">Indicates whether the <paramref name="network"/> is the sidechain.</param> /// <param name="network">The network that we are creating the recovery transaction for.</param> /// <param name="counterChainNetwork">The counterchain network.</param> /// <param name="dataDirPath">The root folder containing the old federation.</param> /// <param name="redeemScript">The new redeem script.</param> /// <param name="password">The password required to generate transactions using the federation wallet.</param> /// <param name="txTime">Any deposits beyond this UTC date will be ignored when selecting coin inputs.</param> /// <returns>A funds recovery transaction that moves funds to the new redeem script.</returns> public FundsRecoveryTransactionModel CreateFundsRecoveryTransaction(bool isSideChain, Network network, Network counterChainNetwork, string dataDirPath, Script redeemScript, string password, DateTime txTime) { var model = new FundsRecoveryTransactionModel() { Network = network, IsSideChain = isSideChain, RedeemScript = redeemScript }; // Get the old redeem script from the wallet file. PayToMultiSigTemplateParameters multisigParams = PayToMultiSigTemplate.Instance.ExtractScriptPubKeyParameters(redeemScript); string theChain = isSideChain ? "sidechain" : "mainchain"; var nodeSettings = new NodeSettings(network, args: new string[] { $"datadir={dataDirPath}", $"redeemscript={redeemScript}", $"-{theChain}" }); var walletFileStorage = new FileStorage <FederationWallet>(nodeSettings.DataFolder.WalletPath); FederationWallet wallet = walletFileStorage.LoadByFileName("multisig_wallet.json"); Script oldRedeemScript = wallet.MultiSigAddress.RedeemScript; PayToMultiSigTemplateParameters oldMultisigParams = PayToMultiSigTemplate.Instance.ExtractScriptPubKeyParameters(oldRedeemScript); model.oldMultisigAddress = oldRedeemScript.Hash.GetAddress(network); model.newMultisigAddress = redeemScript.Hash.GetAddress(network); // Create dummy inputs to avoid errors when constructing FederatedPegSettings. var extraArgs = new Dictionary <string, string>(); extraArgs[FederatedPegSettings.FederationIpsParam] = oldMultisigParams.PubKeys.Select(p => "0.0.0.0".ToIPEndPoint(nodeSettings.Network.DefaultPort)).Join(","); var privateKey = Key.Parse(wallet.EncryptedSeed, password, network); extraArgs[FederatedPegSettings.PublicKeyParam] = privateKey.PubKey.ToHex(network); (new TextFileConfiguration(extraArgs.Select(i => $"{i.Key}={i.Value}").ToArray())).MergeInto(nodeSettings.ConfigReader); model.PubKey = privateKey.PubKey; var dBreezeSerializer = new DBreezeSerializer(network.Consensus.ConsensusFactory); var blockStore = new BlockRepository(network, nodeSettings.DataFolder, nodeSettings.LoggerFactory, dBreezeSerializer); blockStore.Initialize(); var chain = new ChainRepository(nodeSettings.DataFolder, nodeSettings.LoggerFactory, dBreezeSerializer); Block genesisBlock = network.GetGenesis(); ChainedHeader tip = chain.LoadAsync(new ChainedHeader(genesisBlock.Header, genesisBlock.GetHash(), 0)).GetAwaiter().GetResult(); var chainIndexer = new ChainIndexer(network, tip); var nodeLifetime = new NodeLifetime(); IDateTimeProvider dateTimeProvider = DateTimeProvider.Default; var federatedPegSettings = new FederatedPegSettings(nodeSettings); var opReturnDataReader = new OpReturnDataReader(nodeSettings.LoggerFactory, new CounterChainNetworkWrapper(counterChainNetwork)); var walletFeePolicy = new WalletFeePolicy(nodeSettings); var walletManager = new FederationWalletManager(nodeSettings.LoggerFactory, network, chainIndexer, nodeSettings.DataFolder, walletFeePolicy, new AsyncProvider(nodeSettings.LoggerFactory, new Signals(nodeSettings.LoggerFactory, new DefaultSubscriptionErrorHandler(nodeSettings.LoggerFactory)), nodeLifetime), nodeLifetime, dateTimeProvider, federatedPegSettings, new WithdrawalExtractor(nodeSettings.LoggerFactory, federatedPegSettings, opReturnDataReader, network), blockStore); walletManager.Start(); walletManager.EnableFederationWallet(password); if (!walletManager.IsFederationWalletActive()) { throw new ArgumentException($"Could not activate the federation wallet on {network}."); } // Retrieves the unspent outputs in deterministic order. List <Stratis.Features.FederatedPeg.Wallet.UnspentOutputReference> coinRefs = walletManager.GetSpendableTransactionsInWallet().ToList(); // Exclude coins (deposits) beyond the transaction (switch-over) time! coinRefs = coinRefs.Where(r => r.Transaction.CreationTime < txTime).ToList(); if (!coinRefs.Any()) { throw new ArgumentException($"There are no coins to recover from the federation wallet on {network}."); } Money fee = federatedPegSettings.GetWithdrawalTransactionFee(coinRefs.Count()); var builder = new TransactionBuilder(network); builder.AddKeys(privateKey); builder.AddCoins(coinRefs.Select(c => ScriptCoin.Create(network, c.Transaction.Id, (uint)c.Transaction.Index, c.Transaction.Amount, c.Transaction.ScriptPubKey, oldRedeemScript))); // Split the coins into multiple outputs. Money amount = coinRefs.Sum(r => r.Transaction.Amount) - fee; const int numberOfSplits = 10; Money splitAmount = new Money((long)amount / numberOfSplits); var recipients = new List <Stratis.Features.FederatedPeg.Wallet.Recipient>(); for (int i = 0; i < numberOfSplits; i++) { Money sendAmount = (i != (numberOfSplits - 1)) ? splitAmount : amount - splitAmount * (numberOfSplits - 1); builder.Send(redeemScript.PaymentScript, sendAmount); } builder.SetTimeStamp((uint)(new DateTimeOffset(txTime)).ToUnixTimeSeconds()); builder.CoinSelector = new DeterministicCoinSelector(); builder.SendFees(fee); model.tx = builder.BuildTransaction(true); File.WriteAllText(Path.Combine(dataDirPath, $"{network.Name}_{model.PubKey.ToHex(network).Substring(0, 8)}.hex"), model.tx.ToHex(network)); // Merge our transaction with other transactions which have been placed in the data folder. Transaction oldTransaction = model.tx; string namePattern = $"{network.Name}_*.hex"; int sigCount = 1; foreach (string fileName in Directory.EnumerateFiles(dataDirPath, namePattern)) { Transaction incomingPartialTransaction = network.CreateTransaction(File.ReadAllText(fileName)); // Don't merge with self. if (incomingPartialTransaction.GetHash() == oldTransaction.GetHash()) { continue; } // Transaction times must match. if (incomingPartialTransaction is PosTransaction && incomingPartialTransaction.Time != model.tx.Time) { Console.WriteLine($"The locally generated transaction is time-stamped differently from the transaction contained in '{fileName}'. The imported signature can't be used."); continue; } // Combine signatures. Transaction newTransaction = SigningUtils.CheckTemplateAndCombineSignatures(builder, model.tx, new[] { incomingPartialTransaction }); if (oldTransaction.GetHash() == newTransaction.GetHash()) { Console.WriteLine($"The locally generated transaction is not similar to '{fileName}'. The imported signature can't be used."); continue; } model.tx = newTransaction; sigCount++; } Console.WriteLine($"{sigCount} of {multisigParams.SignatureCount} signatures collected for {network.Name}."); if (sigCount >= multisigParams.SignatureCount) { if (builder.Verify(model.tx)) { // Write the transaction to file. File.WriteAllText(Path.Combine(dataDirPath, $"{(txTime > DateTime.Now ? "Preliminary " : "")}{network.Name}Recovery.txt"), model.tx.ToHex(network)); } else { Console.WriteLine("Could not verify the transaction."); } } // Stop the wallet manager to release the database folder. nodeLifetime.StopApplication(); walletManager.Stop(); return(model); }
/// <summary> /// Creates a cold staking withdrawal <see cref="Transaction"/>. /// </summary> /// <remarks> /// Cold staking withdrawal is performed on the wallet that is in the role of the cold staking cold wallet. /// </remarks> /// <param name="walletTransactionHandler">The wallet transaction handler used to build the transaction.</param> /// <param name="receivingAddress">The address that will receive the withdrawal.</param> /// <param name="walletName">The name of the wallet in the role of cold wallet.</param> /// <param name="walletPassword">The wallet password.</param> /// <param name="amount">The amount to remove from cold staking.</param> /// <param name="feeAmount">The fee to pay for cold staking transaction withdrawal.</param> /// <returns>The <see cref="Transaction"/> for cold staking withdrawal.</returns> /// <exception cref="WalletException">Thrown if the receiving address is in a cold staking account in this wallet.</exception> /// <exception cref="ArgumentNullException">Thrown if the receiving address is invalid.</exception> internal Transaction GetColdStakingWithdrawalTransaction(IWalletTransactionHandler walletTransactionHandler, string receivingAddress, string walletName, string walletPassword, Money amount, Money feeAmount) { Guard.NotEmpty(receivingAddress, nameof(receivingAddress)); Guard.NotEmpty(walletName, nameof(walletName)); Guard.NotNull(amount, nameof(amount)); Guard.NotNull(feeAmount, nameof(feeAmount)); Wallet.Types.Wallet wallet = this.GetWalletByName(walletName); // Get the cold staking account. HdAccount coldAccount = this.GetColdStakingAccount(wallet, true); if (coldAccount == null) { this.logger.LogTrace("(-)[COLDSTAKE_ACCOUNT_DOES_NOT_EXIST]"); throw new WalletException("The cold wallet account does not exist."); } // Prevent reusing cold stake addresses as regular withdrawal addresses. if (coldAccount.ExternalAddresses.Concat(coldAccount.InternalAddresses).Any(s => s.Address == receivingAddress || s.Bech32Address == receivingAddress)) { this.logger.LogTrace("(-)[COLDSTAKE_INVALID_COLD_WALLET_ADDRESS_USAGE]"); throw new WalletException("You can't send the money to a cold staking cold wallet account."); } HdAccount hotAccount = this.GetColdStakingAccount(wallet, false); if (hotAccount != null && hotAccount.ExternalAddresses.Concat(hotAccount.InternalAddresses).Any(s => s.Address == receivingAddress || s.Bech32Address == receivingAddress)) { this.logger.LogTrace("(-)[COLDSTAKE_INVALID_HOT_WALLET_ADDRESS_USAGE]"); throw new WalletException("You can't send the money to a cold staking hot wallet account."); } Script destination = null; if (BitcoinWitPubKeyAddress.IsValid(receivingAddress, this.network, out Exception _)) { destination = new BitcoinWitPubKeyAddress(receivingAddress, wallet.Network).ScriptPubKey; } else { // Send the money to the receiving address. destination = BitcoinAddress.Create(receivingAddress, wallet.Network).ScriptPubKey; } // Create the transaction build context (used in BuildTransaction). var accountReference = new WalletAccountReference(walletName, coldAccount.Name); var context = new TransactionBuildContext(wallet.Network) { AccountReference = accountReference, // Specify a dummy change address to prevent a change (internal) address from being created. // Will be changed after the transacton is built and before it is signed. ChangeAddress = coldAccount.ExternalAddresses.First(), TransactionFee = feeAmount, MinConfirmations = 0, Shuffle = false, Sign = false, Recipients = new[] { new Recipient { Amount = amount, ScriptPubKey = destination } }.ToList() }; // Register the cold staking builder extension with the transaction builder. context.TransactionBuilder.Extensions.Add(new ColdStakingBuilderExtension(false)); // Avoid script errors due to missing scriptSig. context.TransactionBuilder.StandardTransactionPolicy.ScriptVerify = null; // Build the transaction according to the settings recorded in the context. Transaction transaction = walletTransactionHandler.BuildTransaction(context); // Map OutPoint to UnspentOutputReference. Dictionary <OutPoint, UnspentOutputReference> mapOutPointToUnspentOutput = this.GetSpendableTransactionsInAccount(accountReference) .ToDictionary(unspent => unspent.ToOutPoint(), unspent => unspent); // Set the cold staking scriptPubKey on the change output. TxOut changeOutput = transaction.Outputs.SingleOrDefault(output => (output.ScriptPubKey != destination) && (output.Value != 0)); if (changeOutput != null) { // Find the largest input. TxIn largestInput = transaction.Inputs.OrderByDescending(input => mapOutPointToUnspentOutput[input.PrevOut].Transaction.Amount).Take(1).Single(); // Set the scriptPubKey of the change output to the scriptPubKey of the largest input. changeOutput.ScriptPubKey = mapOutPointToUnspentOutput[largestInput.PrevOut].Transaction.ScriptPubKey; } // Add keys for signing inputs. This takes time so only add keys for distinct addresses. foreach (UnspentOutputReference item in transaction.Inputs.Select(i => mapOutPointToUnspentOutput[i.PrevOut]).Distinct()) { Script prevscript = item.Transaction.ScriptPubKey; if (prevscript.IsScriptType(ScriptType.P2SH) || prevscript.IsScriptType(ScriptType.P2WSH)) { if (item.Address.RedeemScript == null) { throw new WalletException("Missing redeem script"); } // Provide the redeem script to the builder var scriptCoin = ScriptCoin.Create(this.network, item.ToOutPoint(), new TxOut(item.Transaction.Amount, prevscript), item.Address.RedeemScript); context.TransactionBuilder.AddCoins(scriptCoin); } context.TransactionBuilder.AddKeys(wallet.GetExtendedPrivateKeyForAddress(walletPassword, item.Address)); } // Sign the transaction. context.TransactionBuilder.SignTransactionInPlace(transaction); this.logger.LogTrace("(-):'{0}'", transaction.GetHash()); return(transaction); }
public Transaction BuildRewardTransaction() { // Get the minimum stake confirmations for the current network. int minStakeConfirmations = ((PosConsensusOptions)this.network.Consensus.Options).GetStakeMinConfirmations(this.chainIndexer.Height, this.network); // Take a local copy of the tip. ChainedHeader chainTip = this.chainIndexer.Tip; if (chainTip.Height < minStakeConfirmations) { // If the chain is not at least minStakeConfirmations long then just do nothing. return(null); } // Get the block that is minStakeConfirmations behind the current tip. ChainedHeader chainedHeader = this.chainIndexer.GetHeader(chainTip.Height - minStakeConfirmations); Block maturedBlock = chainedHeader.Block; if (maturedBlock == null) { maturedBlock = this.consensusManager.GetBlockData(chainedHeader.HashBlock).Block; } // If we still don't have the block data, just return. if (maturedBlock == null) { this.logger.LogDebug("Consensus does not have the block data for '{0}'", chainedHeader); return(null); } // As this runs on the mainchain we presume there will be a coinstake transaction in the block (but during the PoW era there obviously may not be). // If not, just do nothing with this block. if (maturedBlock.Transactions.Count < 2 || !maturedBlock.Transactions[1].IsCoinStake) { return(null); } // We are only interested in the coinstake, as it is the only transaction that we expect to contain outputs paying the reward script. Transaction coinStake = maturedBlock.Transactions[1]; // Identify any outputs paying the reward script a nonzero amount. TxOut[] rewardOutputs = coinStake.Outputs.Where(o => o.ScriptPubKey == StraxCoinstakeRule.CirrusRewardScript && o.Value != 0).ToArray(); // This shouldn't be the case but check anyway. if (rewardOutputs.Length == 0) { return(null); } // Build a transaction using these inputs, paying the federation. var builder = new TransactionBuilder(this.network); foreach (TxOut txOutput in rewardOutputs) { // The reward script is P2SH, so we need to inform the builder of the corresponding redeem script to enable it to be spent. var coin = ScriptCoin.Create(this.network, coinStake, txOutput, StraxCoinstakeRule.CirrusRewardScriptRedeem); builder.AddCoins(coin); } // An OP_RETURN for a dummy Cirrus address that tells the sidechain federation they can distribute the transaction. builder.Send(StraxCoinstakeRule.CirrusTransactionTag(this.network.CirrusRewardDummyAddress), Money.Zero); // The mempool will accept a zero-fee transaction as long as it matches this structure, paying to the federation. builder.Send(this.network.Federations.GetOnlyFederation().MultisigScript.PaymentScript, rewardOutputs.Sum(o => o.Value)); Transaction builtTransaction = builder.BuildTransaction(true); // Filter out FeeTooLowPolicyError errors as reward transaction's will not contain any fees. IEnumerable <TransactionPolicyError> errors = builder.Check(builtTransaction).Where(e => !(e is FeeTooLowPolicyError)); if (errors.Any()) { foreach (TransactionPolicyError error in errors) { this.logger.LogWarning("Unable to validate reward claim transaction '{0}', error: {1}", builtTransaction.ToHex(), error.ToString()); } // Not much further can be done at this point. return(null); } this.logger.LogInformation($"Reward distribution transaction built; payment script to federation '{this.network.Federations.GetOnlyFederation().MultisigScript.PaymentScript}'."); return(builtTransaction); }
public void Eight_Of_Fifteen_SufficientlyFunded() { var network = new StratisMain(); const int n = 15; const int m = 8; Key[] keys = new Key[n]; Key ultimateReceiver = new Key(); for (int i = 0; i < n; i++) { keys[i] = new Key(); } Script redeemScript = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(m, keys.Select(x => x.PubKey).ToArray()); const int inputCount = 50; const decimal fundingInputAmount = 100; const decimal fundingAmount = 99; // Must be less than fundingInputAmount. var multiSigCoins = new List <ICoin>(); for (int i = 0; i < inputCount; i++) { var builder = new TransactionBuilder(network); // Build transactions to fund the multisig Transaction funding = builder .AddCoins(GetCoinSource(keys[0], new[] { Money.Coins(fundingInputAmount) })) .AddKeys(keys[0]) .Send(redeemScript.Hash, Money.Coins(fundingAmount)) .SetChange(keys[0].PubKey.Hash) .SendFees(Money.Satoshis(5000)) .BuildTransaction(true); multiSigCoins.Add(ScriptCoin.Create(network, funding, funding.Outputs.To(redeemScript.Hash).First(), redeemScript)); } var fedPegSettings = new FederatedPegSettings(new NodeSettings(network, args: new string[] { "mainchain", "redeemscript=" + redeemScript.ToString(), "publickey=" + keys[0].PubKey.ToHex(), "federationips=0.0.0.0" })); // Construct the withdrawal tx var txBuilder = new TransactionBuilder(network); Transaction tx = txBuilder .AddCoins(multiSigCoins) .AddKeys(keys.Take(m).ToArray()) .Send(ultimateReceiver.PubKey.Hash, Money.Coins(inputCount * fundingAmount - 1)) .SetChange(redeemScript.Hash) .SendFees(fedPegSettings.GetWithdrawalTransactionFee(inputCount)) .BuildTransaction(true); bool verify = txBuilder.Verify(tx, out TransactionPolicyError[] errors); Assert.True(verify); }
private void OnBlockConnected(BlockConnected blockConnected) { // Get the minimum stake confirmations for the current network. int minStakeConfirmations = ((PosConsensusOptions)this.network.Consensus.Options).GetStakeMinConfirmations(this.chainIndexer.Height, this.network); // Take a local copy of the tip. ChainedHeader chainTip = this.chainIndexer.Tip; if (chainTip.Height < minStakeConfirmations) { // If the chain is not at least minStakeConfirmations long then just do nothing. return; } // Get the block that is minStakeConfirmations behind the current tip. ChainedHeader chainedHeader = this.chainIndexer.GetHeader(chainTip.Height - minStakeConfirmations); Block maturedBlock = chainedHeader.Block; // As this runs on the mainchain we presume there will be a coinstake transaction in the block (but during the PoW era there obviously may not be). // If not, just do nothing with this block. if (maturedBlock.Transactions.Count < 2 || !blockConnected.ConnectedBlock.Block.Transactions[1].IsCoinStake) { return; } // We are only interested in the coinstake, as it is the only transaction that we expect to contain outputs paying the reward script. Transaction coinStake = blockConnected.ConnectedBlock.Block.Transactions[1]; // Identify any outputs paying the reward script a nonzero amount. TxOut[] rewardOutputs = coinStake.Outputs.Where(o => o.ScriptPubKey == StraxCoinstakeRule.CirrusRewardScript && o.Value != 0).ToArray(); // This shouldn't be the case but check anyway. if (rewardOutputs.Length == 0) { return; } // Build a transaction using these inputs, paying the federation. var builder = new TransactionBuilder(this.network); foreach (TxOut txOutput in rewardOutputs) { // The reward script is P2SH, so we need to inform the builder of the corresponding redeem script to enable it to be spent. var coin = ScriptCoin.Create(this.network, coinStake, txOutput, StraxCoinstakeRule.CirrusRewardScriptRedeem); builder.AddCoins(coin); } // An OP_RETURN for a dummy Cirrus address that tells the sidechain federation they can distribute the transaction. builder.Send(StraxCoinstakeRule.CirrusTransactionTag, Money.Zero); // TODO: Revisit the handling of fees here - the consensus rules won't allow the fee to be paid from the actual reward builder.Send(this.network.Federations.GetOnlyFederation().MultisigScript, rewardOutputs.Sum(o => o.Value)); Transaction builtTransaction = builder.BuildTransaction(true); TransactionPolicyError[] errors = builder.Check(builtTransaction); if (errors.Length > 0) { foreach (TransactionPolicyError error in errors) { this.logger.LogWarning("Unable to validate reward claim transaction '{0}', error: {1}", builtTransaction.ToHex(), error.ToString()); } // Not much further can be done at this point. return; } // It does not really matter whether the reward has been claimed already, as the transaction will simply be rejected by the other nodes on the network if it has. // So just broadcast it anyway. this.broadcasterManager.BroadcastTransactionAsync(builtTransaction); }