Skip to content

Commit 0d6ef78

Browse files
Implemented cip1855 to derive forging policy keys for minting/burning 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
1 parent f3e4f2b commit 0d6ef78

14 files changed

Lines changed: 674 additions & 162 deletions

.github/workflows/release.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ jobs:
4242
run: |
4343
tag=$(git describe --tags --abbrev=0)
4444
release_name="cscli-$tag-${{ matrix.target }}"
45-
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
45+
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
4646
4747
- name: Upload Build Artifact
4848
uses: actions/upload-artifact@v2

README.md

Lines changed: 203 additions & 121 deletions
Large diffs are not rendered by default.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
namespace Cscli.ConsoleTool;
2+
3+
public record TextEnvelope(string? Type, string? Description, string? CborHex);

Src/ConsoleTool/CommandParser.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ private static ICommand ParseWalletCommands(string intent, string[] args) =>
5353
"wallet key root derive" => BuildCommand<DeriveRootKeyCommand>(args),
5454
"wallet key payment derive" => BuildCommand<DerivePaymentKeyCommand>(args),
5555
"wallet key stake derive" => BuildCommand<DeriveStakeKeyCommand>(args),
56+
"wallet key policy derive" => BuildCommand<DerivePolicyKeyCommand>(args),
5657
"wallet address payment derive" => BuildCommand<DerivePaymentAddressCommand>(args),
5758
"wallet address stake derive" => BuildCommand<DeriveStakeAddressCommand>(args),
5859
_ => new ShowInvalidArgumentCommand(intent)

Src/ConsoleTool/Commands/DerivePaymentKeyCommand.cs

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,24 +39,18 @@ public async ValueTask<CommandResult> ExecuteAsync(CancellationToken ct)
3939
// Write output to CBOR JSON file outputs if optional file paths are supplied
4040
if (!string.IsNullOrWhiteSpace(SigningKeyFile))
4141
{
42-
var paymentSkeyExtendedWithVkeyBytes = paymentSkey.BuildExtendedSkeyWithVerificationKeyBytes();
43-
var skeyCbor = new
44-
{
45-
type = PaymentSKeyJsonTypeField,
46-
description = PaymentSKeyJsonDescriptionField,
47-
cborHex = KeyUtils.BuildCborHexPayload(paymentSkeyExtendedWithVkeyBytes)
48-
};
42+
var skeyCbor = new TextEnvelope(
43+
PaymentExtendedSKeyJsonTypeField,
44+
PaymentSKeyJsonDescriptionField,
45+
KeyUtils.BuildCborHexPayload(paymentSkey.BuildExtendedSkeyWithVerificationKeyBytes()));
4946
await File.WriteAllTextAsync(SigningKeyFile, JsonSerializer.Serialize(skeyCbor, SerialiserOptions), ct).ConfigureAwait(false);
5047
}
5148
if (!string.IsNullOrWhiteSpace(VerificationKeyFile))
5249
{
53-
var paymentVkeyExtendedBytes = paymentVkey.BuildExtendedVkeyBytes();
54-
var vkeyCbor = new
55-
{
56-
type = PaymentVKeyJsonTypeField,
57-
description = PaymentVKeyJsonDescriptionField,
58-
cborHex = KeyUtils.BuildCborHexPayload(paymentVkeyExtendedBytes)
59-
};
50+
var vkeyCbor = new TextEnvelope(
51+
PaymentExtendedVKeyJsonTypeField,
52+
PaymentVKeyJsonDescriptionField,
53+
KeyUtils.BuildCborHexPayload(paymentVkey.BuildExtendedVkeyBytes()));
6054
await File.WriteAllTextAsync(VerificationKeyFile, JsonSerializer.Serialize(vkeyCbor, SerialiserOptions), ct).ConfigureAwait(false);
6155
}
6256
return result;
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
using CardanoSharp.Wallet;
2+
using CardanoSharp.Wallet.Encoding;
3+
using CardanoSharp.Wallet.Enums;
4+
using CardanoSharp.Wallet.Extensions.Models;
5+
using System.Text.Json;
6+
using static Cscli.ConsoleTool.Constants;
7+
8+
namespace Cscli.ConsoleTool.Commands;
9+
10+
// See https://cips.cardano.org/cips/cip1855/
11+
public class DerivePolicyKeyCommand : ICommand
12+
{
13+
public string? Mnemonic { get; init; }
14+
public string Language { get; init; } = DefaultMnemonicLanguage;
15+
public string Passphrase { get; init; } = string.Empty;
16+
public int PolicyIndex { get; init; } = 0;
17+
public string? VerificationKeyFile { get; init; } = null;
18+
public string? SigningKeyFile { get; init; } = null;
19+
20+
public async ValueTask<CommandResult> ExecuteAsync(CancellationToken ct)
21+
{
22+
var (isValid, wordList, validationErrors) = Validate();
23+
if (!isValid)
24+
{
25+
return CommandResult.FailureInvalidOptions(
26+
string.Join(Environment.NewLine, validationErrors));
27+
}
28+
29+
var mnemonicService = new MnemonicService();
30+
try
31+
{
32+
var rootKey = mnemonicService.Restore(Mnemonic, wordList)
33+
.GetRootKey(Passphrase);
34+
var policySkey = rootKey.Derive($"m/1855'/1815'/{PolicyIndex}'");
35+
var policyVkey = policySkey.GetPublicKey(false);
36+
var bech32PolicyKey = Bech32.Encode(policySkey.Key, PolicySigningKeyBech32Prefix);
37+
var result = CommandResult.Success(bech32PolicyKey);
38+
// Write output to CBOR JSON file outputs if optional file paths are supplied
39+
if (!string.IsNullOrWhiteSpace(SigningKeyFile))
40+
{
41+
var skeyCbor = new TextEnvelope(
42+
PaymentExtendedSKeyJsonTypeField, // required for cardano-cli compatibility
43+
PaymentSKeyJsonDescriptionField,
44+
KeyUtils.BuildCborHexPayload(policySkey.BuildExtendedSkeyWithVerificationKeyBytes()));
45+
await File.WriteAllTextAsync(SigningKeyFile, JsonSerializer.Serialize(skeyCbor, SerialiserOptions), ct).ConfigureAwait(false);
46+
}
47+
if (!string.IsNullOrWhiteSpace(VerificationKeyFile))
48+
{
49+
var vkeyCbor = new TextEnvelope(
50+
PaymentExtendedVKeyJsonTypeField, // required for cardano-cli compatibility
51+
PaymentVKeyJsonDescriptionField,
52+
KeyUtils.BuildCborHexPayload(policyVkey.BuildExtendedVkeyBytes()));
53+
await File.WriteAllTextAsync(VerificationKeyFile, JsonSerializer.Serialize(vkeyCbor, SerialiserOptions), ct).ConfigureAwait(false);
54+
}
55+
return result;
56+
}
57+
catch (ArgumentException ex)
58+
{
59+
return CommandResult.FailureInvalidOptions(ex.Message);
60+
}
61+
catch (Exception ex)
62+
{
63+
return CommandResult.FailureUnhandledException("Unexpected error", ex);
64+
}
65+
}
66+
67+
private (
68+
bool isValid,
69+
WordLists wordList,
70+
IReadOnlyCollection<string> validationErrors) Validate()
71+
{
72+
var validationErrors = new List<string>();
73+
if (string.IsNullOrWhiteSpace(Mnemonic))
74+
{
75+
validationErrors.Add(
76+
$"Invalid option --recovery-phrase is required");
77+
}
78+
if (PolicyIndex < 0 || PolicyIndex > MaxDerivationPathIndex)
79+
{
80+
validationErrors.Add(
81+
$"Invalid option --policy-index must be between 0 and {MaxDerivationPathIndex}");
82+
}
83+
if (!string.IsNullOrWhiteSpace(SigningKeyFile)
84+
&& Path.IsPathFullyQualified(SigningKeyFile)
85+
&& !Directory.Exists(Path.GetDirectoryName(SigningKeyFile)))
86+
{
87+
validationErrors.Add(
88+
$"Invalid option --signing-key-file path {SigningKeyFile} does not exist");
89+
}
90+
if (!string.IsNullOrWhiteSpace(VerificationKeyFile)
91+
&& Path.IsPathFullyQualified(VerificationKeyFile)
92+
&& !Directory.Exists(Path.GetDirectoryName(VerificationKeyFile)))
93+
{
94+
validationErrors.Add(
95+
$"Invalid option --verification-key-file path {VerificationKeyFile} does not exist");
96+
}
97+
if (!Enum.TryParse<WordLists>(Language, ignoreCase: true, out var wordlist))
98+
{
99+
validationErrors.Add(
100+
$"Invalid option --language {Language} is not supported");
101+
}
102+
var wordCount = Mnemonic?.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Length;
103+
if (wordCount.HasValue && wordCount > 0 && !ValidMnemonicSizes.Contains(wordCount.Value))
104+
{
105+
validationErrors.Add(
106+
$"Invalid option --recovery-phrase must have the following word count ({string.Join(", ", ValidMnemonicSizes)})");
107+
}
108+
109+
return (!validationErrors.Any(), wordlist, validationErrors);
110+
}
111+
}

Src/ConsoleTool/Commands/DeriveStakeKeyCommand.cs

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,24 +39,19 @@ public async ValueTask<CommandResult> ExecuteAsync(CancellationToken ct)
3939
// Write output to CBOR JSON file outputs if optional file paths are supplied
4040
if (!string.IsNullOrWhiteSpace(SigningKeyFile))
4141
{
42-
var stakeSkeyExtendedWithVkeyBytes = stakeSkey.BuildExtendedSkeyWithVerificationKeyBytes();
43-
var skeyCbor = new
44-
{
45-
type = StakeSKeyJsonTypeField,
46-
description = StakeSKeyJsonDescriptionField,
47-
cborHex = KeyUtils.BuildCborHexPayload(stakeSkeyExtendedWithVkeyBytes)
48-
};
42+
var skeyCbor = new TextEnvelope(
43+
StakeExtendedSKeyJsonTypeField,
44+
StakeSKeyJsonDescriptionField,
45+
KeyUtils.BuildCborHexPayload(stakeSkey.BuildExtendedSkeyWithVerificationKeyBytes()));
4946
await File.WriteAllTextAsync(SigningKeyFile, JsonSerializer.Serialize(skeyCbor, SerialiserOptions), ct).ConfigureAwait(false);
5047
}
5148
if (!string.IsNullOrWhiteSpace(VerificationKeyFile))
5249
{
53-
var stakeVkeyExtendedBytes = stakeVkey.BuildExtendedVkeyBytes();
54-
var vkeyCbor = new
55-
{
56-
type = PaymentVKeyJsonTypeField,
57-
description = PaymentVKeyJsonDescriptionField,
58-
cborHex = KeyUtils.BuildCborHexPayload(stakeVkeyExtendedBytes)
59-
};
50+
// cardano-cli compatibility requires us to use non-extended verification keys
51+
var vkeyCbor = new TextEnvelope(
52+
StakeVKeyJsonTypeField,
53+
StakeVKeyJsonDescriptionField,
54+
KeyUtils.BuildCborHexPayload(stakeVkey.Key));
6055
await File.WriteAllTextAsync(VerificationKeyFile, JsonSerializer.Serialize(vkeyCbor, SerialiserOptions), ct).ConfigureAwait(false);
6156
}
6257
return result;

Src/ConsoleTool/Commands/ShowBaseHelpCommand.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ wallet recovery-phrase generate --size <size> [--language <language>]
2323
wallet key root derive --recovery-phrase ""<string>"" [--language <language>] [--passphrase ""<string>""]
2424
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>]
2525
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>]
26+
wallet key policy derive --recovery-phrase ""<string>"" [--language <language>] [--passphrase ""<string>""] [--policy-index <derivation-index>] [--verification-key-file <string>] [--signing-key-file <string>]
2627
wallet address stake derive --recovery-phrase ""<string>"" --network-type <network-type> [--language <language>] [--passphrase ""<string>""] [--account-index <derivation-index>] [--address-index <derivation-index>]
2728
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>]
2829

Src/ConsoleTool/Constants.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,19 @@ public static class Constants
1111
public const string RootKeyExtendedBech32Prefix = "root_xsk";
1212
public const string PaymentSigningKeyBech32Prefix = "addr_xsk";
1313
public const string StakeSigningKeyBech32Prefix = "stake_xsk";
14-
// JSON CBOR envelopes from cardano-cli
15-
public const string PaymentSKeyJsonTypeField = "PaymentExtendedSigningKeyShelley_ed25519_bip32";
14+
public const string PolicySigningKeyBech32Prefix = "policy_sk";
15+
// JSON CBOR text envelopes from cardano-cli
16+
public const string PaymentSKeyJsonTypeField = "PaymentSigningKeyShelley_ed25519";
17+
public const string PaymentExtendedSKeyJsonTypeField = "PaymentExtendedSigningKeyShelley_ed25519_bip32";
1618
public const string PaymentSKeyJsonDescriptionField = "Payment Signing Key";
17-
public const string PaymentVKeyJsonTypeField = "PaymentExtendedVerificationKeyShelley_ed25519_bip32";
19+
public const string PaymentVKeyJsonTypeField = "PaymentVerificationKeyShelley_ed25519";
20+
public const string PaymentExtendedVKeyJsonTypeField = "PaymentExtendedVerificationKeyShelley_ed25519_bip32";
1821
public const string PaymentVKeyJsonDescriptionField = "Payment Verification Key";
19-
public const string StakeSKeyJsonTypeField = "StakeExtendedSigningKeyShelley_ed25519_bip32";
22+
public const string StakeSKeyJsonTypeField = "StakeSigningKeyShelley_ed25519";
23+
public const string StakeExtendedSKeyJsonTypeField = "StakeExtendedSigningKeyShelley_ed25519_bip32";
2024
public const string StakeSKeyJsonDescriptionField = "Stake Signing Key";
21-
public const string StakeVKeyJsonTypeField = "StakeExtendedVerificationKeyShelley_ed25519_bip32";
25+
public const string StakeVKeyJsonTypeField = "StakeVerificationKeyShelley_ed25519";
26+
public const string StakeExtendedVKeyJsonTypeField = "StakeExtendedVerificationKeyShelley_ed25519_bip32";
2227
public const string StakeVKeyJsonDescriptionField = "Stake Verification Key";
2328
// Validation constraints
2429
public static readonly int[] ValidMnemonicSizes = { 9, 12, 15, 18, 21, 24 };
@@ -36,6 +41,7 @@ public static class Constants
3641
{ "--signing-key-file", "signingKeyFile" },
3742
{ "--account-index", "accountIndex" },
3843
{ "--address-index", "addressIndex" },
44+
{ "--policy-index", "policyIndex" },
3945
{ "--stake-account-index", "stakeAccountIndex" },
4046
{ "--stake-address-index", "stakeAddressIndex" },
4147
{ "--payment-address-type", "paymentAddressType" },

Src/ConsoleTool/Cscli.ConsoleTool.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
<None Include="..\..\README.md" Link="README.md" Pack="true" PackagePath="\" />
3232
</ItemGroup>
3333
<ItemGroup>
34-
<PackageReference Include="CardanoSharp.Wallet" Version="2.0.0" />
34+
<PackageReference Include="CardanoSharp.Wallet" Version="2.0.2" />
3535
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
3636
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" />
3737
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="6.0.0" />

0 commit comments

Comments
 (0)