From 30b6405dfa6cf080a787d5e728043d6911c1d1cb Mon Sep 17 00:00:00 2001 From: rodendron Date: Fri, 17 Sep 2021 19:58:10 +0100 Subject: [PATCH 01/24] Added multisig wallet --- .../Types/WalletMultisig.cs | 292 ++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 src/Features/Blockcore.Features.Wallet/Types/WalletMultisig.cs diff --git a/src/Features/Blockcore.Features.Wallet/Types/WalletMultisig.cs b/src/Features/Blockcore.Features.Wallet/Types/WalletMultisig.cs new file mode 100644 index 000000000..82be6b7c0 --- /dev/null +++ b/src/Features/Blockcore.Features.Wallet/Types/WalletMultisig.cs @@ -0,0 +1,292 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Blockcore.Consensus.ScriptInfo; +using Blockcore.Features.Wallet.Helpers; +using Blockcore.Features.Wallet.Interfaces; +using Blockcore.Networks; +using Blockcore.Utilities.JsonConverters; +using NBitcoin; +using Newtonsoft.Json; + +namespace Blockcore.Features.Wallet.Types +{ + public class WalletMultisig : Wallet + { + public WalletMultisig(List accountsRoot, ICollection blockLocator, byte[] chaincode, DateTimeOffset creationTime, string name, Network network) + { + this.AccountsRoot = (ICollection)accountsRoot; + this.BlockLocator = blockLocator; + this.ChainCode = chaincode; + this.CreationTime = creationTime; + this.EncryptedSeed = string.Empty; + this.IsMultisig = true; + this.IsExtPubKeyWallet = true; + this.Name = name; + this.Network = network; + } + public WalletMultisig() + { + this.IsMultisig = true; + this.EncryptedSeed = string.Empty; + this.AccountsRoot = new List(); + } + + public WalletMultisig(Network network):this() + { + this.Network = network; + } + /// + /// The root of the accounts tree. + /// + [JsonConverter(typeof(AccountRootMultisigConverter))] + [JsonProperty(PropertyName = "accountsRoot")] + public override ICollection AccountsRoot { get; set; } + /// + /// Adds an account to the current wallet. + /// + /// + /// The name given to the account is of the form "account (i)" by default, where (i) is an incremental index starting at 0. + /// According to BIP44, an account at index (i) can only be created when the account at index (i - 1) contains at least one transaction. + /// + /// + /// The password used to decrypt the wallet's . + /// The type of coin this account is for. + /// Creation time of the account to be created. + /// A new HD account. + public HdAccountMultisig AddNewAccount(MultisigScheme scheme, int coinType, DateTimeOffset accountCreationTime) + { + return AddNewAccount(scheme, coinType, this.Network, accountCreationTime); + } + + /// + /// Creates an account as expected in bip-44 account structure. + /// + /// + /// + /// + /// + public HdAccountMultisig AddNewAccount(MultisigScheme multisigScheme, int coinType, Network network, DateTimeOffset accountCreationTime) + { + // Get the current collection of accounts. + var accounts = this.AccountsRoot.FirstOrDefault().Accounts; + + int newAccountIndex = 0; + if (accounts.Any()) + { + newAccountIndex = accounts.Max(a => a.Index) + 1; + } + + string accountHdPath = $"m/45'/{(int)coinType}'/{newAccountIndex}'"; + + var newAccount = new HdAccountMultisig(multisigScheme) + { + Index = newAccountIndex, + ExternalAddresses = new List(), + InternalAddresses = new List(), + Name = $"account {newAccountIndex}", + HdPath = accountHdPath, + CreationTime = accountCreationTime + }; + + accounts.Add(newAccount); + + return newAccount; + } + + } + + /// + /// Provides owerrden mothods for account creation in multisig wallet. + /// + public class AccountRootMultisig : AccountRoot, IAccountRoot + { + public AccountRootMultisig(List accounts, int? coinType, uint256 lastBlockSyncedHash, int? lastBlockSyncedHeight) + { + this.Accounts = (ICollection)accounts; + this.CoinType = coinType; + this.LastBlockSyncedHash = lastBlockSyncedHash; + this.LastBlockSyncedHeight = lastBlockSyncedHeight; + } + + public AccountRootMultisig() + { + this.Accounts = new List(); + } + + [JsonConverter(typeof(HdAccountMultisigConverter))] + [JsonProperty(PropertyName = "accounts")] + public override ICollection Accounts { get; set; } + } + + public class AccountRootMultisigConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + //assume we can convert to anything for now + return true; + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (objectType == typeof(ICollection)) + { + List accountRoots = new List(); + var list = serializer.Deserialize>(reader); + + foreach (var item in list) + { + accountRoots.Add(item); + } + return accountRoots; + } + + return null; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + //use the default serialization - it works fine + serializer.Serialize(writer, value); + } + } + + public class HdAccountMultisigConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + //assume we can convert to anything for now + return true; + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (objectType == typeof(ICollection)) + { + List accounts = new List(); + var list = serializer.Deserialize>(reader); + foreach (var item in list) + { + accounts.Add(item); + } + return accounts; + } + return null; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + //use the default serialization - it works fine + serializer.Serialize(writer, value); + } + } + public class HdAccountMultisig : HdAccount, IHdAccount + { + public HdAccountMultisig(MultisigScheme scheme) + { + this.ExtendedPubKey = "N/A"; + this.MultisigScheme = scheme; + } + + [JsonProperty(PropertyName = "multisigScheme")] + public MultisigScheme MultisigScheme { get; set; } + /// Generates an HD public key derived from an extended public key. + /// + /// The extended public key used to generate child keys. + /// The index of the child key to generate. + /// A value indicating whether the public key to generate corresponds to a change address. + /// + /// An HD public key derived from an extended public key. + /// + + public Script GeneratePublicKey(int hdPathIndex, bool isChange = false) + { + List derivedPubKeys = new List(); + foreach (var xpub in this.MultisigScheme.XPubs) + { + derivedPubKeys.Add(HdOperations.GeneratePublicKey(xpub, hdPathIndex, isChange)); + } + var sortedkeys = LexographicalSort(derivedPubKeys); + + Script redeemScript = PayToMultiSigTemplate.Instance.GenerateScriptPubKey(this.MultisigScheme.Threashold, sortedkeys.ToArray()); + return redeemScript; + } + + + + /// + /// Creates a number of additional addresses in the current account. + /// + /// + /// The name given to the account is of the form "account (i)" by default, where (i) is an incremental index starting at 0. + /// According to BIP44, an account at index (i) can only be created when the account at index (i - 1) contains at least one transaction. + /// + /// Instance of a multisig wallet that allows access to multisig scheme, pubkeys threashold and etc + /// The number of addresses to create. + /// Whether the addresses added are change (internal) addresses or receiving (external) addresses. + /// The created addresses. + public override IEnumerable CreateAddresses(Network network, int addressesQuantity, bool isChange = false) + { + var addresses = isChange ? this.InternalAddresses : this.ExternalAddresses; + + // Get the index of the last address. + int firstNewAddressIndex = 0; + if (addresses.Any()) + { + firstNewAddressIndex = addresses.Max(add => add.Index) + 1; + } + + List addressesCreated = new List(); + for (int i = firstNewAddressIndex; i < firstNewAddressIndex + addressesQuantity; i++) + { + // Generate a new address. + var pubkey = GeneratePublicKey(i, isChange); + BitcoinAddress address = pubkey.Hash.GetAddress(network); + // Add the new address details to the list of addresses. + HdAddress newAddress = new HdAddress + { + Index = i, + HdPath = CreateHdPath((int)this.GetCoinType(), this.Index, i, isChange), + ScriptPubKey = address.ScriptPubKey, + Pubkey = pubkey, + Address = address.ToString(), + RedeemScript = pubkey + //Transactions = new List() + }; + + addresses.Add(newAddress); + addressesCreated.Add(newAddress); + } + + if (isChange) + { + this.InternalAddresses = addresses; + } + else + { + this.ExternalAddresses = addresses; + } + + return addressesCreated; + } + + public static string CreateHdPath(int coinType, int accountIndex, int addressIndex, bool isChange = false) + { + int change = isChange ? 1 : 0; + return $"m/45'/{coinType}'/{accountIndex}'/{change}/{addressIndex}"; + } + + private static IEnumerable LexographicalSort(IEnumerable pubKeys) + { + List list = new List(); + var e = pubKeys.Select(s => s.ToHex()); + var sorted = e.OrderByDescending(s => s.Length).ThenBy(r => r); + foreach (var item in sorted) + { + list.Add(new PubKey(item)); + } + return list; + } + } +} + From 1cd66799b9b5ec0bda9186bfad9d1d4f62586c40 Mon Sep 17 00:00:00 2001 From: rodendron Date: Fri, 17 Sep 2021 20:07:34 +0100 Subject: [PATCH 02/24] Extracting root account interface so that it can be substituted Extracting root account interface so that it can be substituted by multisig wallet classes --- .../Interfaces/IAccountRoot.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/Features/Blockcore.Features.Wallet/Interfaces/IAccountRoot.cs diff --git a/src/Features/Blockcore.Features.Wallet/Interfaces/IAccountRoot.cs b/src/Features/Blockcore.Features.Wallet/Interfaces/IAccountRoot.cs new file mode 100644 index 000000000..b50e7a303 --- /dev/null +++ b/src/Features/Blockcore.Features.Wallet/Interfaces/IAccountRoot.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using Blockcore.Features.Wallet.Database; +using Blockcore.Networks; +using NBitcoin; + +namespace Blockcore.Features.Wallet.Interfaces +{ + public interface IAccountRoot + { + ICollection Accounts { get; set; } + int? CoinType { get; set; } + uint256 LastBlockSyncedHash { get; set; } + int? LastBlockSyncedHeight { get; set; } + + IHdAccount AddNewAccount(ExtPubKey accountExtPubKey, int accountIndex, Network network, DateTimeOffset accountCreationTime); + IHdAccount AddNewAccount(string password, string encryptedSeed, byte[] chainCode, Network network, DateTimeOffset accountCreationTime, int? accountIndex = null, string accountName = null); + IHdAccount CreateAccount(string password, string encryptedSeed, byte[] chainCode, Network network, DateTimeOffset accountCreationTime, int newAccountIndex, string newAccountName = null); + IHdAccount GetAccountByName(string accountName); + IHdAccount GetFirstUnusedAccount(IWalletStore walletStore); + } +} \ No newline at end of file From 31211329b1416a20db05e7f2be2ebf8d8c8adc9a Mon Sep 17 00:00:00 2001 From: rodendron Date: Fri, 17 Sep 2021 20:08:21 +0100 Subject: [PATCH 03/24] Extracting account interface so that it can be substituted Extracting account interface so that it can be substituted by multisig wallet classes --- .../Interfaces/IHdAccount.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/Features/Blockcore.Features.Wallet/Interfaces/IHdAccount.cs diff --git a/src/Features/Blockcore.Features.Wallet/Interfaces/IHdAccount.cs b/src/Features/Blockcore.Features.Wallet/Interfaces/IHdAccount.cs new file mode 100644 index 000000000..1750c591c --- /dev/null +++ b/src/Features/Blockcore.Features.Wallet/Interfaces/IHdAccount.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using Blockcore.Features.Wallet.Database; +using Blockcore.Features.Wallet.Types; +using Blockcore.Networks; +using NBitcoin; + +namespace Blockcore.Features.Wallet.Interfaces +{ + public interface IHdAccount + { + DateTimeOffset CreationTime { get; set; } + string ExtendedPubKey { get; set; } + ICollection ExternalAddresses { get; set; } + string HdPath { get; set; } + int Index { get; set; } + ICollection InternalAddresses { get; set; } + string Name { get; set; } + + IEnumerable CreateAddresses(Network network, int addressesQuantity, bool isChange = false); + (Money ConfirmedAmount, Money UnConfirmedAmount) GetBalances(IWalletStore walletStore, bool excludeColdStakeUtxo); + int GetCoinType(); + IEnumerable GetCombinedAddresses(); + HdAddress GetFirstUnusedChangeAddress(IWalletStore walletStore); + HdAddress GetFirstUnusedReceivingAddress(IWalletStore walletStore); + HdAddress GetLastUsedAddress(IWalletStore walletStore, bool isChange); + IEnumerable GetSpendableTransactions(IWalletStore walletStore, int currentChainHeight, long coinbaseMaturity, int confirmations = 0); + bool IsNormalAccount(); + } +} \ No newline at end of file From 39459b3a4c692cf6f3e0ec221c1f269d5ab3edc1 Mon Sep 17 00:00:00 2001 From: rodendron Date: Fri, 17 Sep 2021 20:09:32 +0100 Subject: [PATCH 04/24] Adding multisig scheme class --- .../Types/MultisigScheme.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/Features/Blockcore.Features.Wallet/Types/MultisigScheme.cs diff --git a/src/Features/Blockcore.Features.Wallet/Types/MultisigScheme.cs b/src/Features/Blockcore.Features.Wallet/Types/MultisigScheme.cs new file mode 100644 index 000000000..beb5dda2d --- /dev/null +++ b/src/Features/Blockcore.Features.Wallet/Types/MultisigScheme.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace Blockcore.Features.Wallet.Types +{ + public class MultisigScheme + { + /// + /// How many signatures will be suffient to move the funds. + /// + [JsonProperty(PropertyName = "threashold")] + public int Threashold { get; set; } + + /// + /// Cosigner extended pubkeys. Intentionally not including any xPriv at this stage as such model is simplest to start with. + /// + [JsonProperty(PropertyName = "xPubs")] + public string[] XPubs { get; set; } + } +} From f8e60a48fb2192d3e0821450344c4e42a1ec6572 Mon Sep 17 00:00:00 2001 From: rodendron Date: Fri, 17 Sep 2021 20:10:14 +0100 Subject: [PATCH 05/24] Testing multisig address derivation --- .../MultisigWalletTest.cs | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/Tests/Blockcore.Features.Wallet.Tests/MultisigWalletTest.cs diff --git a/src/Tests/Blockcore.Features.Wallet.Tests/MultisigWalletTest.cs b/src/Tests/Blockcore.Features.Wallet.Tests/MultisigWalletTest.cs new file mode 100644 index 000000000..ae915601e --- /dev/null +++ b/src/Tests/Blockcore.Features.Wallet.Tests/MultisigWalletTest.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Blockcore.Consensus.ScriptInfo; +using Blockcore.Features.Wallet.Types; +using Blockcore.Networks; +using Blockcore.Networks.XRC; +using Xunit; + +namespace Blockcore.Features.Wallet.Tests +{ + public class MultisigWalletTest + { + private readonly Network network; + private readonly MultisigScheme multisigScheme; + private const int COINTYPE = 10291; + public MultisigWalletTest() + { + this.network = new XRCMain(); + this.multisigScheme = new MultisigScheme() + { + Threashold = 2, + XPubs = new string[] + { + "xpub661MyMwAqRbcFK4g9bHwLNYLmy4JxSFkKNRURL3hJqVYLoZ318eHmEKjMbFxbgDiaMycDk4oixtRkHKgbopRzukzimmyzAb2aEVfAEeJPxt", + "xpub661MyMwAqRbcFVViGz3WGiaYgzsKWKXYduJZ8oR4tnnaRLp2QXMUf5P9Yq6CpJ8zuddutUFyrTkMECqqa1UyjnegVpoiYbcqpJfSCP9GcmG", + "xpub661MyMwAqRbcFYCYnw23jLqPAHMvCfmLeKkmDSLGy6nGpVtJvN9EDoR2qdz8fmvkV8sSa1ZT7j8oyfBgjHKX2nGnESLqSndrM2gJ6TGcXrU", + } + }; + } + + [Fact] + public void Generate1stMultisigAddress() + { + + WalletMultisig wallet = new WalletMultisig(this.network); + var root = new AccountRootMultisig + { + CoinType = COINTYPE + }; + wallet.AccountsRoot.Add(root); + var account = wallet.AddNewAccount(this.multisigScheme, COINTYPE, DateTimeOffset.UtcNow); + + Script redeemScript = account.GeneratePublicKey(0); + Assert.Equal("rbAxG3vTMCuVMWppaobvTajBtUHSiFtkr5", redeemScript.Hash.GetAddress(this.network).ToString()); + } + + [Fact] + public void Generate4SequnetialMultisigRecievingAndChangeAddress() + { + string[] receiving = { "rbAxG3vTMCuVMWppaobvTajBtUHSiFtkr5", "roLKXofxDFrkZqW7kU9aD7j7E6pTajEe16", "rjmcR79w6MatNK3KeHR3FvaEEHngdAKa9h", "reD6MyXPFUaJpyBtJVDgdYf7iN4qHbhzBz" }; + string[] change = { "rXh3PVYpn462fDTAmLiUyKZ1aTWdzr4W9J", "rjNVcwuXKW1d8j8CQbxK4Kim5Hkg7Z4ZhL", "rginUKQEG9XjQVfhJ5ho7P4v9ENrtvF8Uk", "rgeVtHemGFGQzRU2Dhj3gYTgsWcjg4wgG5" }; + + WalletMultisig wallet = new WalletMultisig(this.network); + var root = new AccountRootMultisig + { + CoinType = COINTYPE + }; + wallet.AccountsRoot.Add(root); + var account = wallet.AddNewAccount(this.multisigScheme, COINTYPE, DateTimeOffset.UtcNow); + + for (int i = 0; i < receiving.Length; i++) + { + Script redeemScript = account.GeneratePublicKey(i); + Assert.Equal(receiving[i], redeemScript.Hash.GetAddress(this.network).ToString()); + } + + for (int i = 0; i < change.Length; i++) + { + Script redeemScript = account.GeneratePublicKey(i, true); + Assert.Equal(change[i], redeemScript.Hash.GetAddress(this.network).ToString()); + } + } + } +} From 736a8a4f8391c0894ce7d9d65dc58778fd987ae9 Mon Sep 17 00:00:00 2001 From: rodendron Date: Fri, 17 Sep 2021 20:11:45 +0100 Subject: [PATCH 06/24] Changing wallet class use interfaces instead of concrete classes --- .../Blockcore.Features.Wallet/Types/Wallet.cs | 132 ++++++++++++------ 1 file changed, 93 insertions(+), 39 deletions(-) diff --git a/src/Features/Blockcore.Features.Wallet/Types/Wallet.cs b/src/Features/Blockcore.Features.Wallet/Types/Wallet.cs index 3554c7231..0423e95b7 100644 --- a/src/Features/Blockcore.Features.Wallet/Types/Wallet.cs +++ b/src/Features/Blockcore.Features.Wallet/Types/Wallet.cs @@ -7,6 +7,7 @@ using Blockcore.Features.Wallet.Database; using Blockcore.Features.Wallet.Exceptions; using Blockcore.Features.Wallet.Helpers; +using Blockcore.Features.Wallet.Interfaces; using Blockcore.Networks; using Blockcore.Utilities; using Blockcore.Utilities.JsonConverters; @@ -27,17 +28,17 @@ public class Wallet public const int SpecialPurposeAccountIndexesStart = 100_000_000; /// Filter for identifying normal wallet accounts. - public static Func NormalAccounts = a => a.Index < SpecialPurposeAccountIndexesStart; + public static Func NormalAccounts = a => a.Index < SpecialPurposeAccountIndexesStart; /// Filter for all wallet accounts. - public static Func AllAccounts = a => true; + public static Func AllAccounts = a => true; /// /// Initializes a new instance of the wallet. /// public Wallet() { - this.AccountsRoot = new List(); + this.AccountsRoot = new List(); } [JsonIgnore] @@ -92,15 +93,19 @@ public Wallet() /// /// The root of the accounts tree. /// + [JsonConverter(typeof(AccountRootConverter))] [JsonProperty(PropertyName = "accountsRoot")] - public ICollection AccountsRoot { get; set; } + public virtual ICollection AccountsRoot { get; set; } + + [JsonProperty(PropertyName = "isMultisig")] + public bool IsMultisig { get; set; } /// /// Gets the accounts in the wallet. /// /// An optional filter for filtering the accounts being returned. /// The accounts in the wallet. - public IEnumerable GetAccounts(Func accountFilter = null) + public IEnumerable GetAccounts(Func accountFilter = null) { return this.AccountsRoot.SelectMany(a => a.Accounts).Where(accountFilter ?? NormalAccounts); } @@ -110,7 +115,7 @@ public IEnumerable GetAccounts(Func accountFilter = /// /// The name of the account to retrieve. /// The requested account or null if the account does not exist. - public HdAccount GetAccount(string accountName) + public IHdAccount GetAccount(string accountName) { return this.AccountsRoot.SingleOrDefault()?.GetAccountByName(accountName); } @@ -121,7 +126,7 @@ public HdAccount GetAccount(string accountName) /// The block whose details are used to update the wallet. public void SetLastBlockDetails(ChainedHeader block) { - AccountRoot accountRoot = this.AccountsRoot.SingleOrDefault(); + IAccountRoot accountRoot = this.AccountsRoot.SingleOrDefault(); if (accountRoot == null) { @@ -136,9 +141,9 @@ public void SetLastBlockDetails(ChainedHeader block) /// Gets all the transactions in the wallet. /// /// A list of all the transactions in the wallet. - public IEnumerable GetAllTransactions(Func accountFilter = null) + public IEnumerable GetAllTransactions(Func accountFilter = null) { - List accounts = this.GetAccounts(accountFilter).ToList(); + List accounts = this.GetAccounts(accountFilter).ToList(); // First we iterate normal accounts foreach (TransactionOutputData txData in accounts.Where(a => a.IsNormalAccount()).SelectMany(x => x.ExternalAddresses).SelectMany(x => this.walletStore.GetForAddress(x.Address))) @@ -181,7 +186,7 @@ public IEnumerable GetAllTransactions(FuncA list of all the public keys contained in the wallet. public IEnumerable