Skip to content

Commit

Permalink
Implemented cip1855 to derive forging policy keys for minting/burning…
Browse files Browse the repository at this point in the history
… native tokens (#4)

* Implemented cip1855 to derive forging policy keys for minting/burning native tokens. Switched stake key derivation to non-extended stake vkey outputs for cardano-cli compatibility.

* Corrected documentation for stake key derivation example for cardano-cli compatible text envelope format

* Updated underlying CardanoSharp.Wallet
  • Loading branch information
safestak-keith authored Apr 28, 2022
1 parent f3e4f2b commit 0d6ef78
Show file tree
Hide file tree
Showing 14 changed files with 674 additions and 162 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
run: |
tag=$(git describe --tags --abbrev=0)
release_name="cscli-$tag-${{ matrix.target }}"
dotnet publish Src/ConsoleTool/CsCli.ConsoleTool.csproj -r "${{ matrix.target }}" -c Release -o "$release_name" "-p:PublishSingleFile=true" "-p:AssemblyName=cscli.${{ matrix.target }}" --self-contained true
dotnet publish Src/ConsoleTool/Cscli.ConsoleTool.csproj -r "${{ matrix.target }}" -c Release -o "$release_name" "-p:PublishSingleFile=true" "-p:AssemblyName=cscli.${{ matrix.target }}" --self-contained true
- name: Upload Build Artifact
uses: actions/upload-artifact@v2
Expand Down
324 changes: 203 additions & 121 deletions README.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Src/ConsoleTool/CardanoNodeTypes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Cscli.ConsoleTool;

public record TextEnvelope(string? Type, string? Description, string? CborHex);
1 change: 1 addition & 0 deletions Src/ConsoleTool/CommandParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ private static ICommand ParseWalletCommands(string intent, string[] args) =>
"wallet key root derive" => BuildCommand<DeriveRootKeyCommand>(args),
"wallet key payment derive" => BuildCommand<DerivePaymentKeyCommand>(args),
"wallet key stake derive" => BuildCommand<DeriveStakeKeyCommand>(args),
"wallet key policy derive" => BuildCommand<DerivePolicyKeyCommand>(args),
"wallet address payment derive" => BuildCommand<DerivePaymentAddressCommand>(args),
"wallet address stake derive" => BuildCommand<DeriveStakeAddressCommand>(args),
_ => new ShowInvalidArgumentCommand(intent)
Expand Down
22 changes: 8 additions & 14 deletions Src/ConsoleTool/Commands/DerivePaymentKeyCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,24 +39,18 @@ public async ValueTask<CommandResult> ExecuteAsync(CancellationToken ct)
// Write output to CBOR JSON file outputs if optional file paths are supplied
if (!string.IsNullOrWhiteSpace(SigningKeyFile))
{
var paymentSkeyExtendedWithVkeyBytes = paymentSkey.BuildExtendedSkeyWithVerificationKeyBytes();
var skeyCbor = new
{
type = PaymentSKeyJsonTypeField,
description = PaymentSKeyJsonDescriptionField,
cborHex = KeyUtils.BuildCborHexPayload(paymentSkeyExtendedWithVkeyBytes)
};
var skeyCbor = new TextEnvelope(
PaymentExtendedSKeyJsonTypeField,
PaymentSKeyJsonDescriptionField,
KeyUtils.BuildCborHexPayload(paymentSkey.BuildExtendedSkeyWithVerificationKeyBytes()));
await File.WriteAllTextAsync(SigningKeyFile, JsonSerializer.Serialize(skeyCbor, SerialiserOptions), ct).ConfigureAwait(false);
}
if (!string.IsNullOrWhiteSpace(VerificationKeyFile))
{
var paymentVkeyExtendedBytes = paymentVkey.BuildExtendedVkeyBytes();
var vkeyCbor = new
{
type = PaymentVKeyJsonTypeField,
description = PaymentVKeyJsonDescriptionField,
cborHex = KeyUtils.BuildCborHexPayload(paymentVkeyExtendedBytes)
};
var vkeyCbor = new TextEnvelope(
PaymentExtendedVKeyJsonTypeField,
PaymentVKeyJsonDescriptionField,
KeyUtils.BuildCborHexPayload(paymentVkey.BuildExtendedVkeyBytes()));
await File.WriteAllTextAsync(VerificationKeyFile, JsonSerializer.Serialize(vkeyCbor, SerialiserOptions), ct).ConfigureAwait(false);
}
return result;
Expand Down
111 changes: 111 additions & 0 deletions Src/ConsoleTool/Commands/DerivePolicyKeyCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
using CardanoSharp.Wallet;
using CardanoSharp.Wallet.Encoding;
using CardanoSharp.Wallet.Enums;
using CardanoSharp.Wallet.Extensions.Models;
using System.Text.Json;
using static Cscli.ConsoleTool.Constants;

namespace Cscli.ConsoleTool.Commands;

// See https://cips.cardano.org/cips/cip1855/
public class DerivePolicyKeyCommand : ICommand
{
public string? Mnemonic { get; init; }
public string Language { get; init; } = DefaultMnemonicLanguage;
public string Passphrase { get; init; } = string.Empty;
public int PolicyIndex { get; init; } = 0;
public string? VerificationKeyFile { get; init; } = null;
public string? SigningKeyFile { get; init; } = null;

public async ValueTask<CommandResult> ExecuteAsync(CancellationToken ct)
{
var (isValid, wordList, validationErrors) = Validate();
if (!isValid)
{
return CommandResult.FailureInvalidOptions(
string.Join(Environment.NewLine, validationErrors));
}

var mnemonicService = new MnemonicService();
try
{
var rootKey = mnemonicService.Restore(Mnemonic, wordList)
.GetRootKey(Passphrase);
var policySkey = rootKey.Derive($"m/1855'/1815'/{PolicyIndex}'");
var policyVkey = policySkey.GetPublicKey(false);
var bech32PolicyKey = Bech32.Encode(policySkey.Key, PolicySigningKeyBech32Prefix);
var result = CommandResult.Success(bech32PolicyKey);
// Write output to CBOR JSON file outputs if optional file paths are supplied
if (!string.IsNullOrWhiteSpace(SigningKeyFile))
{
var skeyCbor = new TextEnvelope(
PaymentExtendedSKeyJsonTypeField, // required for cardano-cli compatibility
PaymentSKeyJsonDescriptionField,
KeyUtils.BuildCborHexPayload(policySkey.BuildExtendedSkeyWithVerificationKeyBytes()));
await File.WriteAllTextAsync(SigningKeyFile, JsonSerializer.Serialize(skeyCbor, SerialiserOptions), ct).ConfigureAwait(false);
}
if (!string.IsNullOrWhiteSpace(VerificationKeyFile))
{
var vkeyCbor = new TextEnvelope(
PaymentExtendedVKeyJsonTypeField, // required for cardano-cli compatibility
PaymentVKeyJsonDescriptionField,
KeyUtils.BuildCborHexPayload(policyVkey.BuildExtendedVkeyBytes()));
await File.WriteAllTextAsync(VerificationKeyFile, JsonSerializer.Serialize(vkeyCbor, SerialiserOptions), ct).ConfigureAwait(false);
}
return result;
}
catch (ArgumentException ex)
{
return CommandResult.FailureInvalidOptions(ex.Message);
}
catch (Exception ex)
{
return CommandResult.FailureUnhandledException("Unexpected error", ex);
}
}

private (
bool isValid,
WordLists wordList,
IReadOnlyCollection<string> validationErrors) Validate()
{
var validationErrors = new List<string>();
if (string.IsNullOrWhiteSpace(Mnemonic))
{
validationErrors.Add(
$"Invalid option --recovery-phrase is required");
}
if (PolicyIndex < 0 || PolicyIndex > MaxDerivationPathIndex)
{
validationErrors.Add(
$"Invalid option --policy-index must be between 0 and {MaxDerivationPathIndex}");
}
if (!string.IsNullOrWhiteSpace(SigningKeyFile)
&& Path.IsPathFullyQualified(SigningKeyFile)
&& !Directory.Exists(Path.GetDirectoryName(SigningKeyFile)))
{
validationErrors.Add(
$"Invalid option --signing-key-file path {SigningKeyFile} does not exist");
}
if (!string.IsNullOrWhiteSpace(VerificationKeyFile)
&& Path.IsPathFullyQualified(VerificationKeyFile)
&& !Directory.Exists(Path.GetDirectoryName(VerificationKeyFile)))
{
validationErrors.Add(
$"Invalid option --verification-key-file path {VerificationKeyFile} does not exist");
}
if (!Enum.TryParse<WordLists>(Language, ignoreCase: true, out var wordlist))
{
validationErrors.Add(
$"Invalid option --language {Language} is not supported");
}
var wordCount = Mnemonic?.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Length;
if (wordCount.HasValue && wordCount > 0 && !ValidMnemonicSizes.Contains(wordCount.Value))
{
validationErrors.Add(
$"Invalid option --recovery-phrase must have the following word count ({string.Join(", ", ValidMnemonicSizes)})");
}

return (!validationErrors.Any(), wordlist, validationErrors);
}
}
23 changes: 9 additions & 14 deletions Src/ConsoleTool/Commands/DeriveStakeKeyCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,24 +39,19 @@ public async ValueTask<CommandResult> ExecuteAsync(CancellationToken ct)
// Write output to CBOR JSON file outputs if optional file paths are supplied
if (!string.IsNullOrWhiteSpace(SigningKeyFile))
{
var stakeSkeyExtendedWithVkeyBytes = stakeSkey.BuildExtendedSkeyWithVerificationKeyBytes();
var skeyCbor = new
{
type = StakeSKeyJsonTypeField,
description = StakeSKeyJsonDescriptionField,
cborHex = KeyUtils.BuildCborHexPayload(stakeSkeyExtendedWithVkeyBytes)
};
var skeyCbor = new TextEnvelope(
StakeExtendedSKeyJsonTypeField,
StakeSKeyJsonDescriptionField,
KeyUtils.BuildCborHexPayload(stakeSkey.BuildExtendedSkeyWithVerificationKeyBytes()));
await File.WriteAllTextAsync(SigningKeyFile, JsonSerializer.Serialize(skeyCbor, SerialiserOptions), ct).ConfigureAwait(false);
}
if (!string.IsNullOrWhiteSpace(VerificationKeyFile))
{
var stakeVkeyExtendedBytes = stakeVkey.BuildExtendedVkeyBytes();
var vkeyCbor = new
{
type = PaymentVKeyJsonTypeField,
description = PaymentVKeyJsonDescriptionField,
cborHex = KeyUtils.BuildCborHexPayload(stakeVkeyExtendedBytes)
};
// cardano-cli compatibility requires us to use non-extended verification keys
var vkeyCbor = new TextEnvelope(
StakeVKeyJsonTypeField,
StakeVKeyJsonDescriptionField,
KeyUtils.BuildCborHexPayload(stakeVkey.Key));
await File.WriteAllTextAsync(VerificationKeyFile, JsonSerializer.Serialize(vkeyCbor, SerialiserOptions), ct).ConfigureAwait(false);
}
return result;
Expand Down
1 change: 1 addition & 0 deletions Src/ConsoleTool/Commands/ShowBaseHelpCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ wallet recovery-phrase generate --size <size> [--language <language>]
wallet key root derive --recovery-phrase ""<string>"" [--language <language>] [--passphrase ""<string>""]
wallet key stake derive --recovery-phrase ""<string>"" [--language <language>] [--passphrase ""<string>""] [--account-index <derivation-index>] [--address-index <derivation-index>] [--verification-key-file <string>] [--signing-key-file <string>]
wallet key payment derive --recovery-phrase ""<string>"" [--language <language>] [--passphrase ""<string>""] [--account-index <derivation-index>] [--address-index <derivation-index>] [--verification-key-file <string>] [--signing-key-file <string>]
wallet key policy derive --recovery-phrase ""<string>"" [--language <language>] [--passphrase ""<string>""] [--policy-index <derivation-index>] [--verification-key-file <string>] [--signing-key-file <string>]
wallet address stake derive --recovery-phrase ""<string>"" --network-type <network-type> [--language <language>] [--passphrase ""<string>""] [--account-index <derivation-index>] [--address-index <derivation-index>]
wallet address payment derive --recovery-phrase ""<string>"" --network-type <network-type> --payment-address-type <payment-address-type> [--language <language>] [--passphrase ""<string>""] [--account-index <derivation-index>] [--address-index <derivation-index>] [--stake-account-index <derivation-index>] [--stake-address-index <derivation-index>]
Expand Down
16 changes: 11 additions & 5 deletions Src/ConsoleTool/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,19 @@ public static class Constants
public const string RootKeyExtendedBech32Prefix = "root_xsk";
public const string PaymentSigningKeyBech32Prefix = "addr_xsk";
public const string StakeSigningKeyBech32Prefix = "stake_xsk";
// JSON CBOR envelopes from cardano-cli
public const string PaymentSKeyJsonTypeField = "PaymentExtendedSigningKeyShelley_ed25519_bip32";
public const string PolicySigningKeyBech32Prefix = "policy_sk";
// JSON CBOR text envelopes from cardano-cli
public const string PaymentSKeyJsonTypeField = "PaymentSigningKeyShelley_ed25519";
public const string PaymentExtendedSKeyJsonTypeField = "PaymentExtendedSigningKeyShelley_ed25519_bip32";
public const string PaymentSKeyJsonDescriptionField = "Payment Signing Key";
public const string PaymentVKeyJsonTypeField = "PaymentExtendedVerificationKeyShelley_ed25519_bip32";
public const string PaymentVKeyJsonTypeField = "PaymentVerificationKeyShelley_ed25519";
public const string PaymentExtendedVKeyJsonTypeField = "PaymentExtendedVerificationKeyShelley_ed25519_bip32";
public const string PaymentVKeyJsonDescriptionField = "Payment Verification Key";
public const string StakeSKeyJsonTypeField = "StakeExtendedSigningKeyShelley_ed25519_bip32";
public const string StakeSKeyJsonTypeField = "StakeSigningKeyShelley_ed25519";
public const string StakeExtendedSKeyJsonTypeField = "StakeExtendedSigningKeyShelley_ed25519_bip32";
public const string StakeSKeyJsonDescriptionField = "Stake Signing Key";
public const string StakeVKeyJsonTypeField = "StakeExtendedVerificationKeyShelley_ed25519_bip32";
public const string StakeVKeyJsonTypeField = "StakeVerificationKeyShelley_ed25519";
public const string StakeExtendedVKeyJsonTypeField = "StakeExtendedVerificationKeyShelley_ed25519_bip32";
public const string StakeVKeyJsonDescriptionField = "Stake Verification Key";
// Validation constraints
public static readonly int[] ValidMnemonicSizes = { 9, 12, 15, 18, 21, 24 };
Expand All @@ -36,6 +41,7 @@ public static class Constants
{ "--signing-key-file", "signingKeyFile" },
{ "--account-index", "accountIndex" },
{ "--address-index", "addressIndex" },
{ "--policy-index", "policyIndex" },
{ "--stake-account-index", "stakeAccountIndex" },
{ "--stake-address-index", "stakeAddressIndex" },
{ "--payment-address-type", "paymentAddressType" },
Expand Down
2 changes: 1 addition & 1 deletion Src/ConsoleTool/Cscli.ConsoleTool.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
<None Include="..\..\README.md" Link="README.md" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CardanoSharp.Wallet" Version="2.0.0" />
<PackageReference Include="CardanoSharp.Wallet" Version="2.0.2" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="6.0.0" />
Expand Down
13 changes: 8 additions & 5 deletions Src/ConsoleTool/KeyUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ namespace Cscli.ConsoleTool;

public static class KeyUtils
{
private const int MinimumSigningKeyLength = 32;

public static byte[] BuildBasicSkeyBytes(this PrivateKey prvKey)
=> prvKey.Key.Length > MinimumSigningKeyLength
? prvKey.Key[..MinimumSigningKeyLength] : prvKey.Key;
public static byte[] BuildNonExtendedSkeyWithVerificationKeyBytes(this PrivateKey prvKey)
{
var pubKey = prvKey.GetPublicKey(false);
var skeyBytes = new byte[prvKey.Key.Length + pubKey.Key.Length];
Array.Copy(prvKey.Key, skeyBytes, prvKey.Key.Length);
Array.Copy(pubKey.Key, 0, skeyBytes, prvKey.Key.Length, pubKey.Key.Length);
return skeyBytes;
}

public static byte[] BuildExtendedSkeyBytes(this PrivateKey prvKey)
{
Expand Down
26 changes: 26 additions & 0 deletions Tests/ConsoleTool.UnitTests/CommandParserShould.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,32 @@ public void ParseArgs_Correctly_To_DerivePaymentKeyCommand_When_Options_Are_Vali
payKeyCommand.AddressIndex.Should().Be(expectedAddressIndex);
}

[Theory]
[InlineData(
"wallet key policy derive --recovery-phrase {MNEMONIC}",
"essay choose supply announce entire cart gap duty grow dog similar moral illegal screen jump fury identify world sail arena devote only gas video",
Constants.DefaultMnemonicLanguage,
"", 0)]
[InlineData(
"wallet key policy derive --recovery-phrase {MNEMONIC} --passphrase p455 --language Spanish --policy-index 8",
"dardo demora osadía severo veinte peor humilde óxido secta bocina hallar flauta orador recreo villa fax tienda delito amante lector vicio buitre cosmos zona",
"Spanish",
"p455", 8)]
public void ParseArgs_Correctly_To_DerivePolicyKeyCommand_When_Options_Are_Valid(
string flatArgs, string expectedMnemonic, string expectedLanguage, string expectedPassPhrase, int expectedPolicyIndex)
{
var args = GenerateArgs(flatArgs, expectedMnemonic);

var command = CommandParser.ParseArgsToCommand(args);

var policyKeyCommand = (DerivePolicyKeyCommand)command;
command.Should().BeOfType<DerivePolicyKeyCommand>();
policyKeyCommand.Mnemonic.Should().Be(expectedMnemonic);
policyKeyCommand.Language.Should().Be(expectedLanguage);
policyKeyCommand.Passphrase.Should().Be(expectedPassPhrase);
policyKeyCommand.PolicyIndex.Should().Be(expectedPolicyIndex);
}

[Theory]
[InlineData(
"wallet key stake derive --recovery-phrase {MNEMONIC}",
Expand Down
Loading

0 comments on commit 0d6ef78

Please sign in to comment.