public static void HasReferenceSemantics() { var firstArray = new byte[Blake256Hash.Length]; var secondArray = new byte[Blake256Hash.Length]; for (var i = 0; i < Blake256Hash.Length; i++) { firstArray[i] = (byte)i; secondArray[i] = (byte)i; } var firstHash = new Blake256Hash(firstArray); var secondHash = new Blake256Hash(secondArray); // Value equality Assert.Equal(firstHash, secondHash); Assert.True(firstHash.Equals(secondHash)); Assert.Equal(firstHash.GetHashCode(), secondHash.GetHashCode()); // Reference equality Assert.False(firstHash == secondHash); var hashSet = new HashSet<Blake256Hash> { firstHash }; Assert.True(hashSet.Contains(secondHash)); }
public WalletTransaction(Transaction transaction, Blake256Hash hash, Input[] inputs, Output[] outputs, Amount?fee, DateTimeOffset seenTime) { if (transaction == null) { throw new ArgumentNullException(nameof(transaction)); } if (hash == null) { throw new ArgumentNullException(nameof(hash)); } if (inputs == null) { throw new ArgumentNullException(nameof(inputs)); } if (outputs == null) { throw new ArgumentNullException(nameof(outputs)); } Hash = hash; Inputs = inputs; Outputs = outputs; Fee = fee; Transaction = transaction; SeenTime = seenTime; }
public static void HasReferenceSemantics() { var firstArray = new byte[Blake256Hash.Length]; var secondArray = new byte[Blake256Hash.Length]; for (var i = 0; i < Blake256Hash.Length; i++) { firstArray[i] = (byte)i; secondArray[i] = (byte)i; } var firstHash = new Blake256Hash(firstArray); var secondHash = new Blake256Hash(secondArray); // Value equality Assert.Equal(firstHash, secondHash); Assert.True(firstHash.Equals(secondHash)); Assert.Equal(firstHash.GetHashCode(), secondHash.GetHashCode()); // Reference equality Assert.False(firstHash == secondHash); var hashSet = new HashSet <Blake256Hash> { firstHash }; Assert.True(hashSet.Contains(secondHash)); }
public static Block MarshalBlock(BlockDetails b) { var hash = new Blake256Hash(b.Hash.ToByteArray()); var height = b.Height; var unixTime = b.Timestamp; var transactions = b.Transactions.Select(MarshalWalletTransaction).ToList(); return new Block(hash, height, unixTime, transactions); }
public static Block MarshalBlock(BlockDetails b) { var hash = new Blake256Hash(b.Hash.ToByteArray()); var height = b.Height; var unixTime = b.Timestamp; var transactions = b.Transactions.Select(MarshalWalletTransaction).ToList(); return(new Block(hash, height, unixTime, transactions)); }
public OutPoint(Blake256Hash hash, uint index, byte tree) { if (hash == null) throw new ArgumentNullException(nameof(hash)); Hash = hash; Index = index; Tree = tree; }
public static UnspentOutput MarshalUnspentOutput(FundTransactionResponse.Types.PreviousOutput o) { var txHash = new Blake256Hash(o.TransactionHash.ToByteArray()); var outputIndex = o.OutputIndex; var amount = (Amount)o.Amount; var pkScript = OutputScript.ParseScript(o.PkScript.ToByteArray()); var seenTime = DateTimeOffsetExtras.FromUnixTimeSeconds(o.ReceiveTime); var isFromCoinbase = o.FromCoinbase; return(new UnspentOutput(txHash, outputIndex, amount, pkScript, seenTime, isFromCoinbase)); }
public Block(Blake256Hash hash, int height, long unixTime, List<WalletTransaction> transactions) { if (hash == null) throw new ArgumentNullException(nameof(hash)); if (transactions == null) throw new ArgumentNullException(nameof(transactions)); Identity = new BlockIdentity(hash, height); Timestamp = DateTimeOffsetExtras.FromUnixTimeSeconds(unixTime); Transactions = transactions; }
public static UnspentOutput MarshalUnspentOutput(FundTransactionResponse.Types.PreviousOutput o) { var txHash = new Blake256Hash(o.TransactionHash.ToByteArray()); var outputIndex = o.OutputIndex; var tree = (byte)o.Tree; var amount = (Amount)o.Amount; var pkScript = OutputScript.ParseScript(o.PkScript.ToByteArray()); var seenTime = DateTimeOffsetExtras.FromUnixTimeSeconds(o.ReceiveTime); var isFromCoinbase = o.FromCoinbase; return new UnspentOutput(txHash, outputIndex, tree, amount, pkScript, seenTime, isFromCoinbase); }
public UnspentOutput(Blake256Hash txHash, uint outputIndex, byte tree, Amount amount, OutputScript pkScript, DateTimeOffset seenTime, bool isFromCoinbase) { if (txHash == null) throw new ArgumentNullException(nameof(txHash)); if (pkScript == null) throw new ArgumentNullException(nameof(pkScript)); TransactionHash = txHash; OutputIndex = outputIndex; Tree = tree; Amount = amount; PkScript = pkScript; SeenTime = seenTime; IsFromCoinbase = IsFromCoinbase; }
public Block(Blake256Hash hash, int height, long unixTime, List <WalletTransaction> transactions) { if (hash == null) { throw new ArgumentNullException(nameof(hash)); } if (transactions == null) { throw new ArgumentNullException(nameof(transactions)); } Identity = new BlockIdentity(hash, height); Timestamp = DateTimeOffsetExtras.FromUnixTimeSeconds(unixTime); Transactions = transactions; }
public WalletTransaction(Transaction transaction, Blake256Hash hash, Input[] inputs, Output[] outputs, Amount? fee, DateTimeOffset seenTime) { if (transaction == null) throw new ArgumentNullException(nameof(transaction)); if (hash == null) throw new ArgumentNullException(nameof(hash)); if (inputs == null) throw new ArgumentNullException(nameof(inputs)); if (outputs == null) throw new ArgumentNullException(nameof(outputs)); Hash = hash; Inputs = inputs; Outputs = outputs; Fee = fee; Transaction = transaction; SeenTime = seenTime; }
public UnspentOutput(Blake256Hash txHash, uint outputIndex, Amount amount, OutputScript pkScript, DateTimeOffset seenTime, bool isFromCoinbase) { if (txHash == null) { throw new ArgumentNullException(nameof(txHash)); } if (pkScript == null) { throw new ArgumentNullException(nameof(pkScript)); } TransactionHash = txHash; OutputIndex = outputIndex; Amount = amount; PkScript = pkScript; SeenTime = seenTime; IsFromCoinbase = IsFromCoinbase; }
public static WalletTransaction MarshalWalletTransaction(TransactionDetails tx) { var transaction = Transaction.Deserialize(tx.Transaction.ToByteArray()); var hash = new Blake256Hash(tx.Hash.ToByteArray()); var inputs = tx.Debits .Select(i => new WalletTransaction.Input(i.PreviousAmount, new Account(i.PreviousAccount))) .ToArray(); // There are two kinds of transactions to care about when choosing which outputs // should be created: transactions created by other wallets (inputs.Length == 0) // and those that spend controlled outputs from this wallet (inputs.Length != 0). // If the transaction was created by this wallet, then all outputs (both controlled // and uncontrolled) should be included. Otherwise, uncontrolled outputs can be // ignored since they are not relevant (they could be change outputs for the other // wallet or outputs created for another unrelated wallet). var outputs = inputs.Length == 0 ? tx.Credits.Select(credit => MarshalControlledOutput(credit, transaction.Outputs[credit.Index])).ToArray() : MarshalCombinedOutputs(transaction, tx.Credits); var fee = inputs.Length == transaction.Inputs.Length ? (Amount?)tx.Fee : null; var seenTime = DateTimeOffsetExtras.FromUnixTimeSeconds(tx.Timestamp); return(new WalletTransaction(transaction, hash, inputs, outputs, fee, seenTime)); }
public static WalletTransaction MarshalWalletTransaction(TransactionDetails tx) { var transaction = Transaction.Deserialize(tx.Transaction.ToByteArray()); var hash = new Blake256Hash(tx.Hash.ToByteArray()); var inputs = tx.Debits .Select(i => new WalletTransaction.Input(i.PreviousAmount, new Account(i.PreviousAccount))) .ToArray(); // There are two kinds of transactions to care about when choosing which outputs // should be created: transactions created by other wallets (inputs.Length == 0) // and those that spend controlled outputs from this wallet (inputs.Length != 0). // If the transaction was created by this wallet, then all outputs (both controlled // and uncontrolled) should be included. Otherwise, uncontrolled outputs can be // ignored since they are not relevant (they could be change outputs for the other // wallet or outputs created for another unrelated wallet). var outputs = inputs.Length == 0 ? tx.Credits.Select((o, i) => MarshalControlledOutput(o, transaction.Outputs[i])).ToArray() : MarshalCombinedOutputs(transaction, tx.Credits); var fee = inputs.Length == transaction.Inputs.Length ? (Amount?)tx.Fee : null; var seenTime = DateTimeOffsetExtras.FromUnixTimeSeconds(tx.Timestamp); return new WalletTransaction(transaction, hash, inputs, outputs, fee, seenTime); }
/// <summary> /// Begins synchronization of the client with the remote wallet process. /// A delegate must be passed to be connected to the wallet's ChangesProcessed event to avoid /// a race where additional notifications are processed in the sync task before the caller /// can connect the event. The caller is responsible for disconnecting the delegate from the /// event handler when finished. /// </summary> /// <param name="walletEventHandler">Event handler for changes to wallet as new transactions are processed.</param> /// <returns>The synced Wallet and the Task that is keeping the wallet in sync.</returns> public async Task<Tuple<Mutex<Wallet>, Task>> Synchronize(EventHandler<Wallet.ChangesProcessedEventArgs> walletEventHandler) { if (walletEventHandler == null) throw new ArgumentNullException(nameof(walletEventHandler)); TransactionNotifications notifications; Task notificationsTask; // TODO: Initialization requests need timeouts. // Loop until synchronization did not race on a reorg. while (true) { // Begin receiving notifications for new and removed wallet transactions before // old transactions are downloaded. Any received notifications are saved to // a buffer and are processed after GetAllTransactionsAsync is awaited. notifications = new TransactionNotifications(_channel, _tokenSource.Token); notificationsTask = notifications.ListenAndBuffer(); var networkTask = NetworkAsync(); var accountsTask = AccountsAsync(); var networkResp = await networkTask; var activeBlockChain = BlockChainIdentity.FromNetworkBits(networkResp.ActiveNetwork); var txSetTask = GetTransactionsAsync(Wallet.MinRecentTransactions, Wallet.NumRecentBlocks(activeBlockChain)); var txSet = await txSetTask; var rpcAccounts = await accountsTask; var lastAccountBlockHeight = rpcAccounts.CurrentBlockHeight; var lastAccountBlockHash = new Blake256Hash(rpcAccounts.CurrentBlockHash.ToByteArray()); var lastTxBlock = txSet.MinedTransactions.LastOrDefault(); if (lastTxBlock != null) { var lastTxBlockHeight = lastTxBlock.Height; var lastTxBlockHash = lastTxBlock.Hash; if (lastTxBlockHeight > lastAccountBlockHeight || (lastTxBlockHeight == lastAccountBlockHeight && !lastTxBlockHash.Equals(lastAccountBlockHash))) { _tokenSource.Cancel(); continue; } } // Read all received notifications thus far and determine if synchronization raced // on a chain reorganize. Try again if so. IList<WalletChanges> transactionNotifications; if (notifications.Buffer.TryReceiveAll(out transactionNotifications)) { if (transactionNotifications.Any(r => r.DetachedBlocks.Count != 0)) { _tokenSource.Cancel(); continue; } // Skip all attached block notifications that are in blocks lower than the // block accounts notification. If blocks exist at or past that height, // the first's hash should equal that from the accounts notification. // // This ensures that both notifications contain data that is valid at this // block. var remainingNotifications = transactionNotifications .SelectMany(r => r.AttachedBlocks) .SkipWhile(b => b.Height < lastAccountBlockHeight) .ToList(); if (remainingNotifications.Count != 0) { if (!remainingNotifications[0].Hash.Equals(lastAccountBlockHash)) { _tokenSource.Cancel(); continue; } } // TODO: Merge remaining notifications with the transaction set. // For now, be lazy and start the whole sync over. if (remainingNotifications.Count > 1) { _tokenSource.Cancel(); continue; } } var accounts = rpcAccounts.Accounts.ToDictionary( a => new Account(a.AccountNumber), a => new AccountProperties { AccountName = a.AccountName, TotalBalance = a.TotalBalance, // TODO: uncomment when added to protospec and implemented by wallet. //ImmatureCoinbaseReward = a.ImmatureBalance, ExternalKeyCount = a.ExternalKeyCount, InternalKeyCount = a.InternalKeyCount, ImportedKeyCount = a.ImportedKeyCount, }); Func<AccountsResponse.Types.Account, AccountProperties> createProperties = a => new AccountProperties { AccountName = a.AccountName, TotalBalance = a.TotalBalance, // TODO: uncomment when added to protospec and implemented by wallet. //ImmatureCoinbaseReward = a.ImmatureBalance, ExternalKeyCount = a.ExternalKeyCount, InternalKeyCount = a.InternalKeyCount, ImportedKeyCount = a.ImportedKeyCount, }; // This assumes that all but the last account listed in the RPC response are // BIP0032 accounts, with the same account number as their List index. var bip0032Accounts = rpcAccounts.Accounts.Take(rpcAccounts.Accounts.Count - 1).Select(createProperties).ToList(); var importedAccount = createProperties(rpcAccounts.Accounts.Last()); var chainTip = new BlockIdentity(lastAccountBlockHash, lastAccountBlockHeight); var wallet = new Wallet(activeBlockChain, txSet, bip0032Accounts, importedAccount, chainTip); wallet.ChangesProcessed += walletEventHandler; var walletMutex = new Mutex<Wallet>(wallet); var syncTask = Task.Run(async () => { var client = new WalletService.WalletServiceClient(_channel); var accountsStream = client.AccountNotifications(new AccountNotificationsRequest(), cancellationToken: _tokenSource.Token); var accountChangesTask = accountsStream.ResponseStream.MoveNext(); var txChangesTask = notifications.Buffer.OutputAvailableAsync(); while (true) { var completedTask = await Task.WhenAny(accountChangesTask, txChangesTask); if (!await completedTask) { break; } using (var walletGuard = await walletMutex.LockAsync()) { var w = walletGuard.Instance; if (completedTask == accountChangesTask) { var accountProperties = accountsStream.ResponseStream.Current; var account = new Account(accountProperties.AccountNumber); w.UpdateAccountProperties(account, accountProperties.AccountName, accountProperties.ExternalKeyCount, accountProperties.InternalKeyCount, accountProperties.ImportedKeyCount); accountChangesTask = accountsStream.ResponseStream.MoveNext(); } else if (completedTask == txChangesTask) { var changes = notifications.Buffer.Receive(); w.ApplyTransactionChanges(changes); txChangesTask = notifications.Buffer.OutputAvailableAsync(); } } } await notificationsTask; }); return Tuple.Create(walletMutex, syncTask); } }
public static Transaction Deserialize(byte[] rawTransaction) { if (rawTransaction == null) throw new ArgumentNullException(nameof(rawTransaction)); int version; Input[] inputs; Output[] outputs; uint lockTime; uint expiry; var cursor = new ByteCursor(rawTransaction); try { version = cursor.ReadInt32(); if (version != SupportedVersion) { var reason = $"Unsupported transaction version `{version}`"; throw new EncodingException(typeof(Transaction), cursor, reason); } // Prefix deserialization var prefixInputCount = cursor.ReadCompact(); if (prefixInputCount > TransactionRules.MaxInputs) { var reason = $"Input count {prefixInputCount} exceeds maximum value {TransactionRules.MaxInputs}"; throw new EncodingException(typeof(Transaction), cursor, reason); } inputs = new Input[prefixInputCount]; for (int i = 0; i < (int)prefixInputCount; i++) { var previousHash = new Blake256Hash(cursor.ReadBytes(Blake256Hash.Length)); var previousIndex = cursor.ReadUInt32(); var previousTree = cursor.ReadByte(); var previousOutPoint = new OutPoint(previousHash, previousIndex, previousTree); var sequence = cursor.ReadUInt32(); inputs[i] = Input.CreateFromPrefix(previousOutPoint, sequence); } var outputCount = cursor.ReadCompact(); if (outputCount > TransactionRules.MaxOutputs) { var reason = $"Output count {outputCount} exceeds maximum value {TransactionRules.MaxOutputs}"; throw new EncodingException(typeof(Transaction), cursor, reason); } outputs = new Output[outputCount]; for (int i = 0; i < (int)outputCount; i++) { var amount = (Amount)cursor.ReadInt64(); var outputVersion = cursor.ReadUInt16(); var pkScript = cursor.ReadVarBytes(TransactionRules.MaxPayload); outputs[i] = new Output(amount, outputVersion, pkScript); } lockTime = cursor.ReadUInt32(); expiry = cursor.ReadUInt32(); // Witness deserialization var witnessInputCount = cursor.ReadCompact(); if (witnessInputCount != prefixInputCount) { var reason = $"Input counts in prefix and witness do not match ({prefixInputCount} != {witnessInputCount})"; throw new EncodingException(typeof(Transaction), cursor, reason); } for (int i = 0; i < (int)witnessInputCount; i++) { var inputAmount = (Amount)cursor.ReadInt64(); var blockHeight = cursor.ReadUInt32(); var blockIndex = cursor.ReadUInt32(); var signatureScript = cursor.ReadVarBytes(TransactionRules.MaxPayload); inputs[i] = Input.AddWitness(inputs[i], inputAmount, blockHeight, blockIndex, signatureScript); } } catch (Exception ex) when (!(ex is EncodingException)) { throw new EncodingException(typeof(Transaction), cursor, ex); } return new Transaction(version, inputs, outputs, lockTime, expiry); }
/// <summary> /// Begins synchronization of the client with the remote wallet process. /// A delegate must be passed to be connected to the wallet's ChangesProcessed event to avoid /// a race where additional notifications are processed in the sync task before the caller /// can connect the event. The caller is responsible for disconnecting the delegate from the /// event handler when finished. /// </summary> /// <param name="walletEventHandler">Event handler for changes to wallet as new transactions are processed.</param> /// <returns>The synced Wallet and the Task that is keeping the wallet in sync.</returns> public async Task <Tuple <Wallet, Task> > Synchronize(EventHandler <Wallet.ChangesProcessedEventArgs> walletEventHandler) { if (walletEventHandler == null) { throw new ArgumentNullException(nameof(walletEventHandler)); } TransactionNotifications notifications; Task notificationsTask; // TODO: Initialization requests need timeouts. // Loop until synchronization did not race on a reorg. while (true) { // Begin receiving notifications for new and removed wallet transactions before // old transactions are downloaded. Any received notifications are saved to // a buffer and are processed after GetAllTransactionsAsync is awaited. notifications = new TransactionNotifications(_channel, _tokenSource.Token); notificationsTask = notifications.ListenAndBuffer(); var networkTask = NetworkAsync(); var accountsTask = AccountsAsync(); var networkResp = await networkTask; var activeBlockChain = BlockChainIdentity.FromNetworkBits(networkResp.ActiveNetwork); var txSetTask = GetTransactionsAsync(Wallet.MinRecentTransactions, Wallet.NumRecentBlocks(activeBlockChain)); var txSet = await txSetTask; var rpcAccounts = await accountsTask; var lastAccountBlockHeight = rpcAccounts.CurrentBlockHeight; var lastAccountBlockHash = new Blake256Hash(rpcAccounts.CurrentBlockHash.ToByteArray()); var lastTxBlock = txSet.MinedTransactions.LastOrDefault(); if (lastTxBlock != null) { var lastTxBlockHeight = lastTxBlock.Height; var lastTxBlockHash = lastTxBlock.Hash; if (lastTxBlockHeight > lastAccountBlockHeight || (lastTxBlockHeight == lastAccountBlockHeight && !lastTxBlockHash.Equals(lastAccountBlockHash))) { _tokenSource.Cancel(); continue; } } // Read all received notifications thus far and determine if synchronization raced // on a chain reorganize. Try again if so. IList <WalletChanges> transactionNotifications; if (notifications.Buffer.TryReceiveAll(out transactionNotifications)) { if (transactionNotifications.Any(r => r.DetachedBlocks.Count != 0)) { _tokenSource.Cancel(); continue; } // Skip all attached block notifications that are in blocks lower than the // block accounts notification. If blocks exist at or past that height, // the first's hash should equal that from the accounts notification. // // This ensures that both notifications contain data that is valid at this // block. var remainingNotifications = transactionNotifications .SelectMany(r => r.AttachedBlocks) .SkipWhile(b => b.Height < lastAccountBlockHeight) .ToList(); if (remainingNotifications.Count != 0) { if (!remainingNotifications[0].Hash.Equals(lastAccountBlockHash)) { _tokenSource.Cancel(); continue; } } // TODO: Merge remaining notifications with the transaction set. // For now, be lazy and start the whole sync over. if (remainingNotifications.Count > 1) { _tokenSource.Cancel(); continue; } } var accounts = rpcAccounts.Accounts.ToDictionary( a => new Account(a.AccountNumber), a => new AccountProperties { AccountName = a.AccountName, TotalBalance = a.TotalBalance, // TODO: uncomment when added to protospec and implemented by wallet. //ImmatureCoinbaseReward = a.ImmatureBalance, ExternalKeyCount = a.ExternalKeyCount, InternalKeyCount = a.InternalKeyCount, ImportedKeyCount = a.ImportedKeyCount, }); Func <AccountsResponse.Types.Account, AccountProperties> createProperties = a => new AccountProperties { AccountName = a.AccountName, TotalBalance = a.TotalBalance, // TODO: uncomment when added to protospec and implemented by wallet. //ImmatureCoinbaseReward = a.ImmatureBalance, ExternalKeyCount = a.ExternalKeyCount, InternalKeyCount = a.InternalKeyCount, ImportedKeyCount = a.ImportedKeyCount, }; // This assumes that all but the last account listed in the RPC response are // BIP0032 accounts, with the same account number as their List index. var bip0032Accounts = rpcAccounts.Accounts.Take(rpcAccounts.Accounts.Count - 1).Select(createProperties).ToList(); var importedAccount = createProperties(rpcAccounts.Accounts.Last()); var chainTip = new BlockIdentity(lastAccountBlockHash, lastAccountBlockHeight); var wallet = new Wallet(activeBlockChain, txSet, bip0032Accounts, importedAccount, chainTip); wallet.ChangesProcessed += walletEventHandler; var syncTask = Task.Run(async() => { var client = WalletService.NewClient(_channel); var accountsStream = client.AccountNotifications(new AccountNotificationsRequest(), cancellationToken: _tokenSource.Token); var accountChangesTask = accountsStream.ResponseStream.MoveNext(); var txChangesTask = notifications.Buffer.OutputAvailableAsync(); while (true) { var completedTask = await Task.WhenAny(accountChangesTask, txChangesTask); if (!await completedTask) { break; } if (completedTask == accountChangesTask) { var accountProperties = accountsStream.ResponseStream.Current; var account = new Account(accountProperties.AccountNumber); wallet.UpdateAccountProperties(account, accountProperties.AccountName, accountProperties.ExternalKeyCount, accountProperties.InternalKeyCount, accountProperties.ImportedKeyCount); accountChangesTask = accountsStream.ResponseStream.MoveNext(); } else if (completedTask == txChangesTask) { var changes = notifications.Buffer.Receive(); wallet.ApplyTransactionChanges(changes); } } await notificationsTask; }); return(Tuple.Create(wallet, syncTask)); } }