Created
December 20, 2022 09:58
-
-
Save limebell/13e7b179f7989deeb95909af2c7c86cf to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.Linq; | |
using System.Numerics; | |
using System.Text.Json; | |
using Bencodex; | |
using Bencodex.Types; | |
using Cocona; | |
using Libplanet; | |
using Libplanet.Action; | |
using Libplanet.Blocks; | |
using Libplanet.Crypto; | |
using Libplanet.Extensions.Cocona; | |
using Nekoyume; | |
using Nekoyume.Action; | |
using Nekoyume.Model.State; | |
using NineChronicles.Headless.Executable.IO; | |
using Serilog; | |
using Lib9cUtils = Lib9c.DevExtensions.Utils; | |
namespace NineChronicles.Headless.Executable.Commands | |
{ | |
public class GenesisCommand : CoconaLiteConsoleAppBase | |
{ | |
private const int DefaultCurrencyValue = 10000; | |
private static readonly Codec _codec = new Codec(); | |
private readonly IConsole _console; | |
public GenesisCommand(IConsole console) | |
{ | |
_console = console; | |
} | |
private void ProcessData(DataConfig config, out Dictionary<string, string> tableSheets) | |
{ | |
_console.Out.WriteLine("\nProcessing data for genesis..."); | |
if (string.IsNullOrEmpty(config.TablePath)) | |
{ | |
throw Utils.Error("TablePath is not set."); | |
} | |
tableSheets = Lib9cUtils.ImportSheets(config.TablePath); | |
} | |
private void ProcessCurrency( | |
CurrencyConfig? config, | |
out PrivateKey initialMinter, | |
out List<GoldDistribution> initialDepositList | |
) | |
{ | |
_console.Out.WriteLine("\nProcessing currency for genesis..."); | |
if (config is null) | |
{ | |
_console.Out.WriteLine("CurrencyConfig not provided. Skip setting..."); | |
initialMinter = new PrivateKey(); | |
initialDepositList = new List<GoldDistribution> | |
{ | |
new() | |
{ | |
Address = initialMinter.ToAddress(), AmountPerBlock = DefaultCurrencyValue, | |
StartBlock = 0, EndBlock = 0 | |
} | |
}; | |
return; | |
} | |
if (string.IsNullOrEmpty(config.Value.InitialMinter)) | |
{ | |
_console.Out.WriteLine("Private Key not provided. Create random one..."); | |
initialMinter = new PrivateKey(); | |
} | |
else | |
{ | |
initialMinter = new PrivateKey(config.Value.InitialMinter); | |
} | |
initialDepositList = new List<GoldDistribution>(); | |
if (config.Value.InitialCurrencyDeposit is null || config.Value.InitialCurrencyDeposit.Count == 0) | |
{ | |
_console.Out.WriteLine("Initial currency deposit list not provided. " + | |
$"Give initial {DefaultCurrencyValue} currency to InitialMinter"); | |
initialDepositList.Add(new GoldDistribution | |
{ | |
Address = initialMinter.ToAddress(), | |
AmountPerBlock = DefaultCurrencyValue, | |
StartBlock = 0, | |
EndBlock = 0 | |
}); | |
} | |
else | |
{ | |
initialDepositList = config.Value.InitialCurrencyDeposit; | |
} | |
} | |
private void ProcessAdmin(AdminConfig? config, PrivateKey initialMinter, out AdminState adminState) | |
{ | |
// FIXME: If the `adminState` is not required inside `MineGenesisBlock`, | |
// this logic will be much lighter. | |
_console.Out.WriteLine("\nProcessing admin for genesis..."); | |
adminState = new AdminState(new Address(), 0); | |
if (config is null) | |
{ | |
_console.Out.WriteLine("AdminConfig not provided. Skip admin setting..."); | |
return; | |
} | |
if (config.Value.Activate) | |
{ | |
if (string.IsNullOrEmpty(config.Value.Address)) | |
{ | |
_console.Out.WriteLine("Admin address not provided. Give admin privilege to initialMinter"); | |
adminState = new AdminState(initialMinter.ToAddress(), config.Value.ValidUntil); | |
} | |
} | |
else | |
{ | |
_console.Out.WriteLine("Inactivate Admin. Skip admin setting..."); | |
} | |
_console.Out.WriteLine("Admin config done"); | |
} | |
private void ProcessExtra(ExtraConfig? config, | |
out List<PendingActivationState> pendingActivationStates, | |
out List<Validator> initialValidatorSet | |
) | |
{ | |
_console.Out.WriteLine("\nProcessing extra data for genesis..."); | |
pendingActivationStates = new List<PendingActivationState>(); | |
initialValidatorSet = new List<Validator>(); | |
if (config is null) | |
{ | |
_console.Out.WriteLine("Extra config not provided"); | |
return; | |
} | |
if (!string.IsNullOrEmpty(config.Value.PendingActivationStatePath)) | |
{ | |
string hex = File.ReadAllText(config.Value.PendingActivationStatePath).Trim(); | |
List decoded = (List)_codec.Decode(ByteUtil.ParseHex(hex)); | |
CreatePendingActivations action = new(); | |
action.LoadPlainValue(decoded[1]); | |
pendingActivationStates = action.PendingActivations.Select( | |
pa => new PendingActivationState(pa.Nonce, new PublicKey(pa.PublicKey)) | |
).ToList(); | |
} | |
if (config.Value.InitialValidatorSet is null) | |
{ | |
return; | |
} | |
initialValidatorSet = config.Value.InitialValidatorSet.ToList(); | |
var str = initialValidatorSet.Aggregate(string.Empty, | |
(s, v) => s + "PublicKey: " + v.PublicKey + ", Power: " + v.Power + "\n"); | |
_console.Out.WriteLine($"Initial validator set: {str}"); | |
} | |
[Command(Description = "Mine a new genesis block")] | |
public void Mine( | |
[Argument("CONFIG", Description = "JSON config path to mine genesis block")] | |
string configPath) | |
{ | |
var options = new JsonSerializerOptions | |
{ | |
AllowTrailingCommas = true, | |
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, | |
}; | |
string json = File.ReadAllText(configPath); | |
GenesisConfig genesisConfig = JsonSerializer.Deserialize<GenesisConfig>(json, options); | |
try | |
{ | |
ProcessData(genesisConfig.Data, out var tableSheets); | |
ProcessCurrency(genesisConfig.Currency, out var initialMinter, out var initialDepositList); | |
ProcessAdmin(genesisConfig.Admin, initialMinter, out var adminState); | |
ProcessExtra(genesisConfig.Extra, | |
out var pendingActivationStates, | |
out var initialValidatorSet); | |
// Mine genesis block | |
_console.Out.WriteLine("\nMining genesis block...\n"); | |
Block<PolymorphicAction<ActionBase>> block = BlockHelper.ProposeGenesisBlock( | |
tableSheets: tableSheets, | |
goldDistributions: initialDepositList.ToArray(), | |
pendingActivationStates: pendingActivationStates.ToArray(), | |
adminState: adminState, | |
privateKey: initialMinter | |
); | |
Lib9cUtils.ExportBlock(block, "genesis-block"); | |
if (genesisConfig.Admin?.Activate == true) | |
{ | |
if (string.IsNullOrEmpty(genesisConfig.Admin.Value.Address)) | |
{ | |
_console.Out.WriteLine("Initial minter has admin privilege. Keep this account in secret."); | |
} | |
else | |
{ | |
_console.Out.WriteLine("Admin privilege has been granted to given admin address. " + | |
"Keep this account in secret."); | |
} | |
} | |
if (genesisConfig.Currency?.InitialCurrencyDeposit is null || | |
genesisConfig.Currency.Value.InitialCurrencyDeposit.Count == 0) | |
{ | |
if (string.IsNullOrEmpty(genesisConfig.Currency?.InitialMinter)) | |
{ | |
_console.Out.WriteLine("No currency data provided. Initial minter gets initial deposition.\n" + | |
"Please check `initial_deposit.csv` file to get detailed info."); | |
File.WriteAllText("initial_deposit.csv", | |
"Address,PrivateKey,AmountPerBlock,StartBlock,EndBlock\n"); | |
File.AppendAllText("initial_deposit.csv", | |
$"{initialMinter.ToAddress()},{ByteUtil.Hex(initialMinter.ByteArray)},{DefaultCurrencyValue},0,0"); | |
} | |
else | |
{ | |
_console.Out.WriteLine("No initial deposit data provided. " + | |
"Initial minter you provided gets initial deposition."); | |
} | |
} | |
_console.Out.WriteLine("\nGenesis block created."); | |
} | |
catch (Exception e) | |
{ | |
throw Utils.Error(e.Message); | |
} | |
} | |
#pragma warning disable S3459 | |
/// <summary> | |
/// Game data to set into genesis block. | |
/// </summary> | |
/// <seealso cref="GenesisConfig"/> | |
[Serializable] | |
private struct DataConfig | |
{ | |
/// <value>A path of game data table directory.</value> | |
public string TablePath { get; set; } | |
} | |
/// <summary> | |
/// Currency related configurations.<br/> | |
/// Set initial minter(Tx signer) and/or initial currency depositions.<br/> | |
/// If not provided, default values will set. | |
/// </summary> | |
[Serializable] | |
private struct CurrencyConfig | |
{ | |
/// <value> | |
/// Private Key of initial currency minter.<br/> | |
/// If not provided, a new private key will be created and used.<br/> | |
/// </value> | |
public string? InitialMinter { get; set; } // PrivateKey, not Address | |
/// <value> | |
/// Initial currency deposition list.<br/> | |
/// If you leave it to empty list or even not provide, the `InitialMinter` will get 10000 currency.<br.> | |
/// You can see newly created deposition info in <c>initial_deposit.csv</c> file. | |
/// </value> | |
public List<GoldDistribution>? InitialCurrencyDeposit { get; set; } | |
} | |
/// <summary> | |
/// Admin related configurations.<br/> | |
/// If not provided, no admin will be set. | |
/// </summary> | |
[Serializable] | |
private struct AdminConfig | |
{ | |
/// <value>Whether active admin address or not.</value> | |
public bool Activate { get; set; } | |
/// <value> | |
/// Address to give admin privilege.<br/> | |
/// If <c>Activate</c> is <c>true</c> and no <c>Address</c> provided, the <see cref="CurrencyConfig.InitialMinter"/> will get admin privilege. | |
/// </value> | |
public string Address { get; set; } | |
/// <value> | |
/// The block count to persist admin privilege.<br/> | |
/// After this block, admin will no longer be admin. | |
/// </value> | |
public long ValidUntil { get; set; } | |
} | |
/// <summary> | |
/// Extra configurations. | |
/// </summary> | |
[Serializable] | |
private struct ExtraConfig | |
{ | |
/// <value> | |
/// Dump file path of pending activation state created using <c>9c-tools</c><br/> | |
/// This will set activation codes that can be used to genesis block. <br/> | |
/// See <a href="https://github.com/planetarium/lib9c/blob/development/.Lib9c.Tools/SubCommand/Tx.cs">Tx.cs</a> to create activation key. | |
/// </value> | |
public string? PendingActivationStatePath { get; set; } | |
public List<Validator>? InitialValidatorSet { get; set; } | |
} | |
[Serializable] | |
private struct Validator | |
{ | |
public string PublicKey { get; set; } | |
public int Power { get; set; } | |
} | |
/// <summary> | |
/// Config to mine new genesis block. | |
/// </summary> | |
/// <list type="table"> | |
/// <listheader> | |
/// <term>Config</term> | |
/// <description>Description</description> | |
/// </listheader> | |
/// <item> | |
/// <term><see cref="DataConfig">Data</see></term> | |
/// <description>Required. Sets game data to genesis block.</description> | |
/// </item> | |
/// <item> | |
/// <term><see cref="CurrencyConfig">Currency</see></term> | |
/// <description>Optional. Sets initial currency mint/deposition data to genesis block.</description> | |
/// </item> | |
/// <item> | |
/// <term><see cref="AdminConfig">Admin</see></term> | |
/// <description>Optional. Sets game admin and lifespan to genesis block.</description> | |
/// </item> | |
/// <item> | |
/// <term><see cref="ExtraConfig">Extra</see></term> | |
/// <description>Optional. Sets extra data (e.g. activation keys) to genesis block.</description> | |
/// </item> | |
/// </list> | |
[Serializable] | |
private struct GenesisConfig | |
{ | |
public DataConfig Data { get; set; } // Required | |
public CurrencyConfig? Currency { get; set; } | |
public AdminConfig? Admin { get; set; } | |
public ExtraConfig? Extra { get; set; } | |
} | |
#pragma warning restore S3459 | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment