public async Task <PubKey> JoinFederationAsync(JoinFederationRequestModel request, CancellationToken cancellationToken) { // Get the address pub key hash. var address = BitcoinAddress.Create(request.CollateralAddress, this.counterChainSettings.CounterChainNetwork); KeyId addressKey = PayToPubkeyHashTemplate.Instance.ExtractScriptPubKeyParameters(address.ScriptPubKey); // Get mining key. var keyTool = new KeyTool(this.settings.DataFolder); Key minerKey = keyTool.LoadPrivateKey(); if (minerKey == null) { throw new Exception($"The private key file ({KeyTool.KeyFileDefaultName}) has not been configured."); } var expectedCollateralAmount = CollateralFederationMember.GetCollateralAmountForPubKey(this.network, minerKey.PubKey); Money collateralAmount = new Money(expectedCollateralAmount, MoneyUnit.BTC); var joinRequest = new JoinFederationRequest(minerKey.PubKey, collateralAmount, addressKey); // Populate the RemovalEventId. var collateralFederationMember = new CollateralFederationMember(minerKey.PubKey, false, joinRequest.CollateralAmount, request.CollateralAddress); byte[] federationMemberBytes = (this.network.Consensus.ConsensusFactory as CollateralPoAConsensusFactory).SerializeFederationMember(collateralFederationMember); var votingManager = this.fullNode.NodeService <VotingManager>(); Poll poll = votingManager.GetFinishedPolls().FirstOrDefault(x => x.IsExecuted && x.VotingData.Key == VoteKey.KickFederationMember && x.VotingData.Data.SequenceEqual(federationMemberBytes)); joinRequest.RemovalEventId = (poll == null) ? Guid.Empty : new Guid(poll.PollExecutedBlockData.Hash.ToBytes().TakeLast(16).ToArray()); // Get the signature by calling the counter-chain "signmessage" API. var signMessageRequest = new SignMessageRequest() { Message = joinRequest.SignatureMessage, WalletName = request.CollateralWalletName, Password = request.CollateralWalletPassword, ExternalAddress = request.CollateralAddress }; var walletClient = new WalletClient(this.loggerFactory, this.httpClientFactory, $"http://{this.counterChainSettings.CounterChainApiHost}", this.counterChainSettings.CounterChainApiPort); string signature = await walletClient.SignMessageAsync(signMessageRequest, cancellationToken); if (signature == null) { throw new Exception("Operation was cancelled during call to counter-chain to sign the collateral address."); } joinRequest.AddSignature(signature); var walletTransactionHandler = this.fullNode.NodeService <IWalletTransactionHandler>(); var encoder = new JoinFederationRequestEncoder(this.loggerFactory); Transaction trx = JoinFederationRequestBuilder.BuildTransaction(walletTransactionHandler, this.network, joinRequest, encoder, request.WalletName, request.WalletAccount, request.WalletPassword); var walletService = this.fullNode.NodeService <IWalletService>(); await walletService.SendTransaction(new SendTransactionRequest(trx.ToHex()), cancellationToken); return(minerKey.PubKey); }
public static byte[] GetFederationMemberBytes(JoinFederationRequest joinRequest, Network network, Network counterChainNetwork) { Script collateralScript = PayToPubkeyHashTemplate.Instance.GenerateScriptPubKey(joinRequest.CollateralMainchainAddress); BitcoinAddress bitcoinAddress = collateralScript.GetDestinationAddress(counterChainNetwork); var collateralFederationMember = new CollateralFederationMember(joinRequest.PubKey, false, joinRequest.CollateralAmount, bitcoinAddress.ToString()); return((network.Consensus.ConsensusFactory as CollateralPoAConsensusFactory).SerializeFederationMember(collateralFederationMember)); }
public void CanSerializeAndDeserializeFederationMember() { var federationMember = new CollateralFederationMember(new Key().PubKey, new Money(999), "addr1"); byte[] serializedBytes = this.factory.SerializeFederationMember(federationMember); var deserializedMember = this.factory.DeserializeFederationMember(serializedBytes) as CollateralFederationMember; Assert.Equal(federationMember, deserializedMember); }
public override IFederationMember DeserializeFederationMember(byte[] serializedBytes) { string json = Encoding.ASCII.GetString(serializedBytes); CirrusCollateralFederationMemberModel model = Serializer.ToObject <CirrusCollateralFederationMemberModel>(json); var member = new CollateralFederationMember(new PubKey(model.PubKeyHex), new Money(model.CollateralAmountSatoshis), model.CollateralMainchainAddress); return(member); }
public void EnterStraxEra(List <IFederationMember> modifiedFederation) { // If we are accessing blocks prior to STRAX activation then the IsMultisigMember values for the members may be different. for (int i = 0; i < modifiedFederation.Count; i++) { bool shouldBeMultisigMember = ((PoANetwork)this.network).StraxMiningMultisigMembers.Contains(modifiedFederation[i].PubKey); var member = (CollateralFederationMember)modifiedFederation[i]; if (member.IsMultisigMember != shouldBeMultisigMember) { // Clone the member if we will be changing the flag. modifiedFederation[i] = new CollateralFederationMember(member.PubKey, shouldBeMultisigMember, member.CollateralAmount, member.CollateralMainchainAddress); } } }
/// <inheritdoc/> public override void CheckTransaction(MempoolValidationContext context) { if (context.Transaction.IsCoinBase || context.Transaction.IsCoinStake) { return; } // This will raise a consensus error if there is a voting request and it can't be decoded. JoinFederationRequest request = JoinFederationRequestBuilder.Deconstruct(context.Transaction, this.encoder); if (request == null) { return; } if (this.federationManager.IsMultisigMember(request.PubKey)) { this.logger.LogTrace("(-)[INVALID_MULTISIG_VOTING]"); context.State.Fail(MempoolErrors.VotingRequestInvalidMultisig, $"{context.Transaction.GetHash()} has an invalid voting request for a multisig member.").Throw(); } // Check collateral amount. var collateralAmount = CollateralFederationMember.GetCollateralAmountForPubKey((PoANetwork)this.network, request.PubKey); if (request.CollateralAmount.ToDecimal(MoneyUnit.BTC) != collateralAmount) { this.logger.LogTrace("(-)[INVALID_COLLATERAL_REQUIREMENT]"); context.State.Fail(MempoolErrors.InvalidCollateralRequirement, $"{context.Transaction.GetHash()} has a voting request with an invalid colateral requirement.").Throw(); } if (!(this.federationManager is CollateralFederationManager federationManager)) { return; } // Prohibit re-use of collateral addresses. Script script = PayToPubkeyHashTemplate.Instance.GenerateScriptPubKey(request.CollateralMainchainAddress); string collateralAddress = script.GetDestinationAddress(this.network).ToString(); CollateralFederationMember owner = federationManager.CollateralAddressOwner(this.votingManager, VoteKey.AddFederationMember, collateralAddress); if (owner != null && owner.PubKey != request.PubKey) { this.logger.LogTrace("(-)[INVALID_COLLATERAL_REUSE]"); context.State.Fail(MempoolErrors.VotingRequestInvalidCollateralReuse, $"{context.Transaction.GetHash()} has a voting request that's re-using collateral.").Throw(); } }
public void CheckTransaction(Transaction transaction, int height) { if (transaction.IsCoinBase || transaction.IsCoinStake) { return; } // This will raise a consensus error if there is a voting request and it can't be decoded. JoinFederationRequest request = JoinFederationRequestBuilder.Deconstruct(transaction, this.encoder); if (request == null) { return; } if (this.federationManager.IsMultisigMember(request.PubKey)) { // Can't cast votes in relation to a multisig member. this.Logger.LogTrace("(-)[INVALID_VOTING_ON_MULTISIG_MEMBER]"); PoAConsensusErrors.InvalidVotingOnMultiSig.Throw(); } // Check collateral amount. var collateralAmount = CollateralFederationMember.GetCollateralAmountForPubKey((PoANetwork)this.network, request.PubKey); if (request.CollateralAmount.ToDecimal(MoneyUnit.BTC) != collateralAmount) { this.Logger.LogTrace("(-)[INVALID_COLLATERAL_REQUIREMENT]"); PoAConsensusErrors.InvalidCollateralRequirement.Throw(); } // Prohibit re-use of collateral addresses. if (height >= ((PoAConsensusOptions)(this.network.Consensus.Options)).Release1100ActivationHeight) { Script script = PayToPubkeyHashTemplate.Instance.GenerateScriptPubKey(request.CollateralMainchainAddress); string collateralAddress = script.GetDestinationAddress(this.counterChainNetwork).ToString(); CollateralFederationMember owner = this.federationManager.CollateralAddressOwner(this.votingManager, VoteKey.AddFederationMember, collateralAddress); if (owner != null) { this.Logger.LogTrace("(-)[INVALID_COLLATERAL_REUSE]"); PoAConsensusErrors.VotingRequestInvalidCollateralReuse.Throw(); } } }
public CollateralFederationMember CollateralAddressOwner(VotingManager votingManager, VoteKey voteKey, string address) { CollateralFederationMember member = (this.federationMembers.Cast <CollateralFederationMember>().FirstOrDefault(x => x.CollateralMainchainAddress == address)); if (member != null) { return(member); } List <Poll> finishedPolls = votingManager.GetFinishedPolls(); member = finishedPolls .Where(x => !x.IsExecuted && x.VotingData.Key == voteKey) .Select(x => this.GetMember(x.VotingData)) .FirstOrDefault(x => x.CollateralMainchainAddress == address); if (member != null) { return(member); } List <Poll> pendingPolls = votingManager.GetPendingPolls(); member = pendingPolls .Where(x => x.VotingData.Key == voteKey) .Select(x => this.GetMember(x.VotingData)) .FirstOrDefault(x => x.CollateralMainchainAddress == address); if (member != null) { return(member); } List <VotingData> scheduledVotes = votingManager.GetScheduledVotes(); member = scheduledVotes .Where(x => x.Key == voteKey) .Select(x => this.GetMember(x)) .FirstOrDefault(x => x.CollateralMainchainAddress == address); return(member); }
private IActionResult VoteAddKickFedMember(CollateralFederationMemberModel request, bool addMember) { Guard.NotNull(request, nameof(request)); if (!this.ModelState.IsValid) { return(ModelStateErrors.BuildErrorResponse(this.ModelState)); } if (!this.fedManager.IsFederationMember) { return(ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, "Only federation members can vote", string.Empty)); } try { var key = new PubKey(request.PubKeyHex); if (this.fedManager.IsMultisigMember(key)) { return(ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, "Multisig members can't be voted on", string.Empty)); } IFederationMember federationMember = new CollateralFederationMember(key, false, new Money(request.CollateralAmountSatoshis), request.CollateralMainchainAddress); byte[] fedMemberBytes = (this.network.Consensus.ConsensusFactory as PoAConsensusFactory).SerializeFederationMember(federationMember); this.votingManager.ScheduleVote(new VotingData() { Key = addMember ? VoteKey.AddFederationMember : VoteKey.KickFederationMember, Data = fedMemberBytes }); return(this.Ok()); } catch (Exception e) { this.logger.LogError("Exception occurred: {0}", e.ToString()); return(ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, "There was a problem executing a command.", e.ToString())); } }
/// <summary>Checks that whomever mined this block is participating in any pending polls to vote-in new federation members.</summary> public override Task RunAsync(RuleContext context) { // Determine the members that this node is currently in favor of adding. List <Poll> pendingPolls = this.ruleEngine.VotingManager.GetPendingPolls(); var encoder = new JoinFederationRequestEncoder(this.loggerFactory); IEnumerable <PubKey> newMembers = pendingPolls .Where(p => p.VotingData.Key == VoteKey.AddFederationMember && (p.PollStartBlockData == null || p.PollStartBlockData.Height <= context.ValidationContext.ChainedHeaderToValidate.Height) && p.PubKeysHexVotedInFavor.Any(pk => pk == this.federationManager.CurrentFederationKey.PubKey.ToHex())) .Select(p => ((CollateralFederationMember)this.consensusFactory.DeserializeFederationMember(p.VotingData.Data)).PubKey); if (!newMembers.Any()) { return(Task.CompletedTask); } // Determine who mined the block. PubKey blockMiner = this.slotsManager.GetFederationMemberForBlock(context.ValidationContext.ChainedHeaderToValidate, this.votingManager).PubKey; // Check that the miner is in favor of adding the same member(s). Dictionary <string, bool> checkList = newMembers.ToDictionary(x => x.ToHex(), x => false); foreach (CollateralFederationMember member in pendingPolls .Where(p => p.VotingData.Key == VoteKey.AddFederationMember && p.PubKeysHexVotedInFavor.Any(pk => pk == blockMiner.ToHex())) .Select(p => (CollateralFederationMember)this.consensusFactory.DeserializeFederationMember(p.VotingData.Data))) { checkList[member.PubKey.ToHex()] = true; } if (!checkList.Any(c => !c.Value)) { return(Task.CompletedTask); } // Otherwise check that the miner is including those votes now. Transaction coinbase = context.ValidationContext.BlockToValidate.Transactions[0]; byte[] votingDataBytes = this.votingDataEncoder.ExtractRawVotingData(coinbase); if (votingDataBytes != null) { List <VotingData> votingDataList = this.votingDataEncoder.Decode(votingDataBytes); foreach (VotingData votingData in votingDataList) { var member = (CollateralFederationMember)this.consensusFactory.DeserializeFederationMember(votingData.Data); var expectedCollateralAmount = CollateralFederationMember.GetCollateralAmountForPubKey((PoANetwork)this.network, member.PubKey); // Check collateral amount. if (member.CollateralAmount.ToDecimal(MoneyUnit.BTC) != expectedCollateralAmount) { this.logger.LogTrace("(-)[INVALID_COLLATERAL_REQUIREMENT]"); PoAConsensusErrors.InvalidCollateralRequirement.Throw(); } // Can't be a multisig member. if (member.IsMultisigMember) { this.logger.LogTrace("(-)[INVALID_MULTISIG_VOTING]"); PoAConsensusErrors.VotingRequestInvalidMultisig.Throw(); } checkList[member.PubKey.ToHex()] = true; } } // If any outstanding votes have not been included throw a consensus error. if (checkList.Any(c => !c.Value)) { PoAConsensusErrors.BlockMissingVotes.Throw(); } return(Task.CompletedTask); }
private void OnBlockConnected(BlockConnected blockConnectedData) { if (!(this.network.Consensus.ConsensusFactory is CollateralPoAConsensusFactory consensusFactory)) { return; } List <Transaction> transactions = blockConnectedData.ConnectedBlock.Block.Transactions; var encoder = new JoinFederationRequestEncoder(this.loggerFactory); for (int i = 0; i < transactions.Count; i++) { Transaction tx = transactions[i]; try { JoinFederationRequest request = JoinFederationRequestBuilder.Deconstruct(tx, encoder); if (request == null) { continue; } // Skip if the member already exists. if (this.votingManager.IsFederationMember(request.PubKey)) { continue; } // Check if the collateral amount is valid. decimal collateralAmount = request.CollateralAmount.ToDecimal(MoneyUnit.BTC); var expectedCollateralAmount = CollateralFederationMember.GetCollateralAmountForPubKey((PoANetwork)this.network, request.PubKey); if (collateralAmount != expectedCollateralAmount) { this.logger.LogDebug("Ignoring voting collateral amount '{0}', when expecting '{1}'.", collateralAmount, expectedCollateralAmount); continue; } // Fill in the request.removalEventId (if any). Script collateralScript = PayToPubkeyHashTemplate.Instance.GenerateScriptPubKey(request.CollateralMainchainAddress); var collateralFederationMember = new CollateralFederationMember(request.PubKey, false, request.CollateralAmount, collateralScript.GetDestinationAddress(this.counterChainNetwork).ToString()); byte[] federationMemberBytes = consensusFactory.SerializeFederationMember(collateralFederationMember); // Nothing to do if already voted. if (this.votingManager.AlreadyVotingFor(VoteKey.AddFederationMember, federationMemberBytes)) { this.logger.LogDebug("Skipping because already voted for adding '{0}'.", request.PubKey.ToHex()); continue; } // Populate the RemovalEventId. Poll poll = this.votingManager.GetFinishedPolls().FirstOrDefault(x => x.IsExecuted && x.VotingData.Key == VoteKey.KickFederationMember && x.VotingData.Data.SequenceEqual(federationMemberBytes)); request.RemovalEventId = (poll == null) ? Guid.Empty : new Guid(poll.PollExecutedBlockData.Hash.ToBytes().TakeLast(16).ToArray()); // Check the signature. PubKey key = PubKey.RecoverFromMessage(request.SignatureMessage, request.Signature); if (key.Hash != request.CollateralMainchainAddress) { this.logger.LogDebug("Invalid collateral address validation signature for joining federation via transaction '{0}'", tx.GetHash()); continue; } // Vote to add the member. this.logger.LogDebug("Voting to add federation member '{0}'.", request.PubKey.ToHex()); this.votingManager.ScheduleVote(new VotingData() { Key = VoteKey.AddFederationMember, Data = federationMemberBytes }); } catch (Exception err) { this.logger.LogDebug(err.Message); } } }
public async Task <PubKey> JoinFederationAsync(JoinFederationRequestModel request, CancellationToken cancellationToken) { // Wait until the node is synced before joining. if (this.initialBlockDownloadState.IsInitialBlockDownload()) { throw new Exception($"Please wait until the node is synced with the network before attempting to join the federation."); } // First ensure that this collateral address isnt already present in the federation. if (this.federationManager.GetFederationMembers().IsCollateralAddressRegistered(request.CollateralAddress)) { throw new Exception($"The provided collateral address '{request.CollateralAddress}' is already present in the federation."); } // Get the address pub key hash. BitcoinAddress address = BitcoinAddress.Create(request.CollateralAddress, this.counterChainSettings.CounterChainNetwork); KeyId addressKey = PayToPubkeyHashTemplate.Instance.ExtractScriptPubKeyParameters(address.ScriptPubKey); // Get mining key. var keyTool = new KeyTool(this.nodeSettings.DataFolder); Key minerKey = keyTool.LoadPrivateKey(); if (minerKey == null) { throw new Exception($"The private key file ({KeyTool.KeyFileDefaultName}) has not been configured or is not present."); } var expectedCollateralAmount = CollateralFederationMember.GetCollateralAmountForPubKey(this.network, minerKey.PubKey); var joinRequest = new JoinFederationRequest(minerKey.PubKey, new Money(expectedCollateralAmount, MoneyUnit.BTC), addressKey); // Populate the RemovalEventId. SetLastRemovalEventId(joinRequest, GetFederationMemberBytes(joinRequest, this.network, this.counterChainSettings.CounterChainNetwork), this.votingManager); // Get the signature by calling the counter-chain "signmessage" API. var signMessageRequest = new SignMessageRequest() { Message = joinRequest.SignatureMessage, WalletName = request.CollateralWalletName, Password = request.CollateralWalletPassword, ExternalAddress = request.CollateralAddress }; var walletClient = new WalletClient(this.httpClientFactory, $"http://{this.counterChainSettings.CounterChainApiHost}", this.counterChainSettings.CounterChainApiPort); try { string signature = await walletClient.SignMessageAsync(signMessageRequest, cancellationToken); joinRequest.AddSignature(signature); } catch (Exception err) { throw new Exception($"The call to sign the join federation request failed: '{err.Message}'."); } IWalletTransactionHandler walletTransactionHandler = this.fullNode.NodeService <IWalletTransactionHandler>(); var encoder = new JoinFederationRequestEncoder(); JoinFederationRequestResult result = JoinFederationRequestBuilder.BuildTransaction(walletTransactionHandler, this.network, joinRequest, encoder, request.WalletName, request.WalletAccount, request.WalletPassword); if (result.Transaction == null) { throw new Exception(result.Errors); } IWalletService walletService = this.fullNode.NodeService <IWalletService>(); await walletService.SendTransaction(new SendTransactionRequest(result.Transaction.ToHex()), cancellationToken); return(minerKey.PubKey); }
public void OnBlockConnected(BlockConnected blockConnectedData) { if (!(this.network.Consensus.ConsensusFactory is CollateralPoAConsensusFactory consensusFactory)) { return; } // Only mining federation members vote to include new members. if (this.federationManager.CurrentFederationKey?.PubKey == null) { return; } List <IFederationMember> modifiedFederation = null; List <Transaction> transactions = blockConnectedData.ConnectedBlock.Block.Transactions; var encoder = new JoinFederationRequestEncoder(); for (int i = 0; i < transactions.Count; i++) { Transaction tx = transactions[i]; try { JoinFederationRequest request = JoinFederationRequestBuilder.Deconstruct(tx, encoder); if (request == null) { continue; } // Skip if the member already exists. if (this.votingManager.IsFederationMember(request.PubKey)) { continue; } // Only mining federation members vote to include new members. modifiedFederation ??= this.votingManager.GetModifiedFederation(blockConnectedData.ConnectedBlock.ChainedHeader); if (!modifiedFederation.Any(m => m.PubKey == this.federationManager.CurrentFederationKey.PubKey)) { this.logger.LogDebug($"Ignoring as member '{this.federationManager.CurrentFederationKey.PubKey}' is not part of the federation at block '{blockConnectedData.ConnectedBlock.ChainedHeader}'."); return; } // Check if the collateral amount is valid. decimal collateralAmount = request.CollateralAmount.ToDecimal(MoneyUnit.BTC); var expectedCollateralAmount = CollateralFederationMember.GetCollateralAmountForPubKey((PoANetwork)this.network, request.PubKey); if (collateralAmount != expectedCollateralAmount) { this.logger.LogDebug("Ignoring voting collateral amount '{0}', when expecting '{1}'.", collateralAmount, expectedCollateralAmount); continue; } // Fill in the request.removalEventId (if any). byte[] federationMemberBytes = JoinFederationRequestService.GetFederationMemberBytes(request, this.network, this.counterChainNetwork); // Nothing to do if already voted. if (this.votingManager.AlreadyVotingFor(VoteKey.AddFederationMember, federationMemberBytes)) { this.logger.LogDebug("Skipping because already voted for adding '{0}'.", request.PubKey.ToHex()); continue; } // Populate the RemovalEventId. JoinFederationRequestService.SetLastRemovalEventId(request, federationMemberBytes, this.votingManager); // Check the signature. PubKey key = PubKey.RecoverFromMessage(request.SignatureMessage, request.Signature); if (key.Hash != request.CollateralMainchainAddress) { this.logger.LogDebug("Invalid collateral address validation signature for joining federation via transaction '{0}'", tx.GetHash()); continue; } // Vote to add the member. this.logger.LogDebug("Voting to add federation member '{0}'.", request.PubKey.ToHex()); this.votingManager.ScheduleVote(new VotingData() { Key = VoteKey.AddFederationMember, Data = federationMemberBytes }); } catch (Exception err) { this.logger.LogError(err.Message); } } }