Пример #1
0
        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);
        }
Пример #2
0
        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.");
        }
Пример #3
0
        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.");
        }
Пример #4
0
        /// <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);
        }
Пример #5
0
        /// <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);
        }
Пример #6
0
        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);
        }
Пример #8
0
        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);
        }