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);
        }
Пример #2
0
        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);
        }
Пример #5
0
        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()));
            }
        }
Пример #10
0
        /// <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);
                }
            }
        }
Пример #12
0
        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);
                }
            }
        }