Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Masternode] Offline signing endpoints #526

Open
wants to merge 3 commits into
base: release/1.1.0.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,50 @@ public sealed class JoinFederationRequestModel
[JsonProperty(PropertyName = "walletAccount")]
public string WalletAccount { get; set; }
}

/// <summary>
/// Contains the collateral address for which we want to build the join message for signing.
/// </summary>
public sealed class GetJoinMessageModel
{
/// <summary>
/// The collateral address.
/// </summary>
[Required(ErrorMessage = "The collateral address is required.")]
[JsonProperty(PropertyName = "collateralAddress")]
public string CollateralAddress { get; set; }
}

/// <summary>
/// A class containing the necessary parameters for a join federation request with a signed message.
/// </summary>
public sealed class JoinFederationRequestWithSignature
{
/// <summary>
/// The collateral address.
/// </summary>
[Required(ErrorMessage = "The collateral address is required.")]
[JsonProperty(PropertyName = "collateralAddress")]
public string CollateralAddress { get; set; }

/// <summary>The name of the wallet which will supply the fee on the Cirrus network.</summary>
[Required(ErrorMessage = "The fee wallet name is required")]
[JsonProperty(PropertyName = "walletName")]
public string WalletName { get; set; }

/// <summary>The password of the wallet which will supply the fee on the Cirrus network.</summary>
[Required(ErrorMessage = "The fee wallet password is required")]
[JsonProperty(PropertyName = "walletPassword")]
public string WalletPassword { get; set; }

/// <summary>The account of the wallet which will supply the fee on the Cirrus network.</summary>
[Required(ErrorMessage = "The fee wallet account is required")]
[JsonProperty(PropertyName = "walletAccount")]
public string WalletAccount { get; set; }

/// <summary>The join federation message signature as signed by the collateral address key.</summary>
[Required(ErrorMessage = "The signature is required")]
[JsonProperty(PropertyName = "signature")]
public string Signature { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ namespace Stratis.Features.PoA.Voting
{
public interface IJoinFederationRequestService
{
JoinFederationRequest BuildJoinFederationRequest(string collateralAddress);
Task<PubKey> BroadcastSignedJoinRequestAsync(JoinFederationRequest request, string walletName, string walletPassword, string walletAccount, CancellationToken cancellationToken);
Task<PubKey> JoinFederationAsync(JoinFederationRequestModel request, CancellationToken cancellationToken);
}

Expand Down Expand Up @@ -47,29 +49,7 @@ public JoinFederationRequestService(ICounterChainSettings counterChainSettings,
public async Task<PubKey> JoinFederationAsync(JoinFederationRequestModel request, CancellationToken cancellationToken)
{
// 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 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);
Poll poll = this.votingManager.GetApprovedPolls().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());
JoinFederationRequest joinRequest = BuildJoinFederationRequest(request.CollateralAddress);

// Get the signature by calling the counter-chain "signmessage" API.
var signMessageRequest = new SignMessageRequest()
Expand All @@ -92,16 +72,76 @@ public async Task<PubKey> JoinFederationAsync(JoinFederationRequestModel request
throw new Exception($"The call to sign the join federation request failed: '{err.Message}'.");
}

if (!VerifyCollateralSignature(joinRequest))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You check this inside BroadcastSignedJoinRequestAsync too - as that method gets called in two places it makes sense to keep the if inside it only

{
throw new Exception($"The signature from the collateral address {joinRequest.CollateralMainchainAddress} is invalid");
}

return await BroadcastSignedJoinRequestAsync(joinRequest, request.WalletName, request.WalletPassword, request.WalletAccount, cancellationToken);
}

public async Task<PubKey> BroadcastSignedJoinRequestAsync(JoinFederationRequest request, string walletName, string walletPassword, string walletAccount, CancellationToken cancellationToken)
{
if(!VerifyCollateralSignature(request))
{
throw new Exception($"The signature from the collateral address {request.CollateralMainchainAddress} is invalid");
}

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);
JoinFederationRequestResult result = JoinFederationRequestBuilder.BuildTransaction(walletTransactionHandler, this.network, request, encoder, walletName, walletAccount, 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;
return GetMinerKey().PubKey;
}

public JoinFederationRequest BuildJoinFederationRequest(string collateralAddress)
{
Key minerKey = GetMinerKey();

var expectedCollateralAmount = CollateralFederationMember.GetCollateralAmountForPubKey(this.network, minerKey.PubKey);

var collateralAmount = new Money(expectedCollateralAmount, MoneyUnit.BTC);

BitcoinAddress address = BitcoinAddress.Create(collateralAddress, this.counterChainSettings.CounterChainNetwork);
KeyId addressKey = PayToPubkeyHashTemplate.Instance.ExtractScriptPubKeyParameters(address.ScriptPubKey);

var joinRequest = new JoinFederationRequest(minerKey.PubKey, collateralAmount, addressKey);

// Populate the RemovalEventId.
var collateralFederationMember = new CollateralFederationMember(minerKey.PubKey, false, joinRequest.CollateralAmount, collateralAddress);

byte[] federationMemberBytes = (this.network.Consensus.ConsensusFactory as CollateralPoAConsensusFactory).SerializeFederationMember(collateralFederationMember);
Poll poll = this.votingManager.GetApprovedPolls().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());
return joinRequest;
}

private Key GetMinerKey()
{
// 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.");
return minerKey;
}

/// <summary>
/// Returns true if the join request message is signed by the given collateral address.
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
public bool VerifyCollateralSignature(JoinFederationRequest request)
{
BitcoinPubKeyAddress bitcoinPubKeyAddress = new BitcoinPubKeyAddress(request.CollateralMainchainAddress, this.counterChainSettings.CounterChainNetwork);
return bitcoinPubKeyAddress.VerifyMessage(request.SignatureMessage, request.Signature);
}
}
}
94 changes: 94 additions & 0 deletions src/Stratis.Features.Collateral/CollateralController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ namespace Stratis.Features.Collateral
public static class CollateralRouteEndPoint
{
public const string JoinFederation = "joinfederation";
public const string JoinFederationSigned = "joinfederationsigned";
public const string GetJoinMessageForSigning = "getjoinmessageforsigning";
}

/// <summary>Controller providing operations on collateral federation members.</summary>
Expand Down Expand Up @@ -82,5 +84,97 @@ public async Task<IActionResult> JoinFederationAsync([FromBody] JoinFederationRe
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
}
}

/// <summary>
/// Called by a miner wanting to join the federation.
/// </summary>
/// <param name="request">See <see cref="JoinFederationRequestModel"></see>.</param>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy pasted?

/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>An instance of <see cref="JoinFederationResponseModel"/>.</returns>
/// <response code="200">Returns a valid response.</response>
/// <response code="400">Unexpected exception occurred</response>
[Route(CollateralRouteEndPoint.GetJoinMessageForSigning)]
[HttpPost]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType((int)HttpStatusCode.InternalServerError)]
public async Task<IActionResult> GetJoinMessageForSigning([FromBody] GetJoinMessageModel request, CancellationToken cancellationToken = default)
{
Guard.NotNull(request, nameof(request));

// Checks that the request is valid.
if (!this.ModelState.IsValid)
{
this.logger.Trace("(-)[MODEL_STATE_INVALID]");
return ModelStateErrors.BuildErrorResponse(this.ModelState);
}

if (!(this.network.Consensus.Options as PoAConsensusOptions).AutoKickIdleMembers)
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, "Error", "This feature is currently disabled.");

try
{
var joinFederationRequest = this.joinFederationRequestService.BuildJoinFederationRequest(request.CollateralAddress);

return this.Json(joinFederationRequest.SignatureMessage);
}
catch (Exception e)
{
this.logger.Error("Exception occurred: {0}", e.ToString());
this.logger.Trace("(-)[ERROR]");
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
}
}

/// <summary>
/// Called by a miner wanting to join the federation.
/// </summary>
/// <param name="request">See <see cref="JoinFederationRequestModel"></see>.</param>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JoinFederationRequestWithSignature?

/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>An instance of <see cref="JoinFederationResponseModel"/>.</returns>
/// <response code="200">Returns a valid response.</response>
/// <response code="400">Unexpected exception occurred</response>
[Route(CollateralRouteEndPoint.JoinFederationSigned)]
[HttpPost]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType((int)HttpStatusCode.InternalServerError)]
public async Task<IActionResult> JoinFederationSigned([FromBody] JoinFederationRequestWithSignature request, CancellationToken cancellationToken = default)
{
Guard.NotNull(request, nameof(request));

// Checks that the request is valid.
if (!this.ModelState.IsValid)
{
this.logger.Trace("(-)[MODEL_STATE_INVALID]");
return ModelStateErrors.BuildErrorResponse(this.ModelState);
}

if (!(this.network.Consensus.Options as PoAConsensusOptions).AutoKickIdleMembers)
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, "Error", "This feature is currently disabled.");

try
{
var joinFederationRequest = this.joinFederationRequestService.BuildJoinFederationRequest(request.CollateralAddress);

joinFederationRequest.AddSignature(request.Signature);

PubKey minerPubKey = await this.joinFederationRequestService.BroadcastSignedJoinRequestAsync(joinFederationRequest, request.WalletName, request.WalletPassword, request.WalletAccount, cancellationToken);

var model = new JoinFederationResponseModel
{
MinerPublicKey = minerPubKey.ToHex()
};

this.logger.Trace("(-):'{0}'", model);
return this.Json(model);
}
catch (Exception e)
{
this.logger.Error("Exception occurred: {0}", e.ToString());
this.logger.Trace("(-)[ERROR]");
return ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString());
}
}
}
}