public void Given_AWatchedAddress_When_ATransactionIsReceivedInABlock_ThenTransactionDataIsAddedToTheAddress() { // Arrange. DataFolder dataFolder = CreateDataFolder(this); // Create the wallet to watch. WatchOnlyWallet wallet = this.CreateAndPersistAWatchOnlyWallet(dataFolder); // Create the address to watch. Script newScript = BitcoinAddress.Create("mnSmvy2q4dFNKQF18EBsrZrS7WEy6CieEE", this.networkTestNet).ScriptPubKey; string newAddress = newScript.GetDestinationAddress(this.networkTestNet).ToString(); // Create a transaction to be received. string transactionHex = "010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff230384041200fe0eb3a959fe1af507000963676d696e6572343208000000000000000000ffffffff02155e8b09000000001976a9144bfe90c8e6c6352c034b3f57d50a9a6e77a62a0788ac0000000000000000266a24aa21a9ed0bc6e4bfe82e04a1c52e66b72b199c5124794dd8c3c368f6ab95a0ba6cde277d0120000000000000000000000000000000000000000000000000000000000000000000000000"; Transaction transaction = this.networkTestNet.CreateTransaction(transactionHex); Block block = this.networkTestNet.Consensus.ConsensusFactory.CreateBlock(); block.AddTransaction(transaction); block.UpdateMerkleRoot(); // Act. var walletManager = new WatchOnlyWalletManager(DateTimeProvider.Default, this.LoggerFactory.Object, this.networkTestNet, dataFolder, this.signals); walletManager.Initialize(); walletManager.WatchAddress("mnSmvy2q4dFNKQF18EBsrZrS7WEy6CieEE"); walletManager.ProcessBlock(block); // Assert. WatchOnlyWallet returnedWallet = walletManager.GetWatchOnlyWallet(); Assert.NotNull(returnedWallet); WatchedAddress addressInWallet = returnedWallet.WatchedAddresses[newScript.ToString()]; Assert.NotNull(addressInWallet); Assert.False(addressInWallet.Transactions.IsEmpty); Assert.Single(addressInWallet.Transactions); WatchTransactionData transactionExpected = addressInWallet.Transactions.Single().Value; Assert.Equal(transactionHex, transactionExpected.Hex); Assert.NotNull(transactionExpected.BlockHash); Assert.NotNull(transactionExpected.MerkleProof); }
public void Given_AnAddressIsPassed_When_WatchAddressIsCalled_ThenAnAddressIsAddedToTheWatchList() { DataFolder dataFolder = CreateDataFolder(this); WatchOnlyWallet wallet = this.CreateAndPersistAWatchOnlyWallet(dataFolder); var walletManager = new WatchOnlyWalletManager(DateTimeProvider.Default, this.LoggerFactory.Object, this.networkTestNet, dataFolder, this.signals); walletManager.Initialize(); // create the wallet Script newScript = new Key().ScriptPubKey; string newAddress = newScript.GetDestinationAddress(this.networkTestNet).ToString(); walletManager.WatchAddress(newAddress); WatchOnlyWallet returnedWallet = walletManager.GetWatchOnlyWallet(); Assert.NotNull(returnedWallet); Assert.True(returnedWallet.WatchedAddresses.ContainsKey(newScript.ToString())); }
public void Configure(string name, string version, DateTimeOffset?created) { this.name = name; this.version = version; // If no value is supplied, we will use the current time. if (!created.HasValue) { created = this.dateTimeProvider.GetTimeOffset(); } var watchOnlyWallet = new WatchOnlyWallet { Network = this.network, CoinType = CoinType.Stratis, CreationTime = created.Value }; this.wallet = watchOnlyWallet; }
/// <inheritdoc /> public void ProcessBlock(int height, Block block) { // Check for any server registration transactions if (block.Transactions != null) { foreach (Transaction tx in block.Transactions) { // Minor optimisation to disregard transactions that cannot be registrations if (tx.Outputs.Count < 2) { continue; } // Check if the transaction has the Breeze registration marker output (literal text BREEZE_REGISTRATION_MARKER) if (tx.Outputs[0].ScriptPubKey.ToHex().ToLower().Equals("6a1a425245455a455f524547495354524154494f4e5f4d41524b4552")) { this.logger.LogDebug("Received a new registration transaction: " + tx.GetHash()); try { RegistrationToken registrationToken = new RegistrationToken(); registrationToken.ParseTransaction(tx, this.network); if (!registrationToken.Validate(this.network)) { this.logger.LogDebug("Registration token failed validation"); continue; } MerkleBlock merkleBlock = new MerkleBlock(block, new uint256[] { tx.GetHash() }); RegistrationRecord registrationRecord = new RegistrationRecord(DateTime.Now, Guid.NewGuid(), tx.GetHash().ToString(), tx.ToHex(), registrationToken, merkleBlock.PartialMerkleTree, height); // Ignore protocol versions outside the accepted bounds if ((registrationRecord.Record.ProtocolVersion < MIN_PROTOCOL_VERSION) || (registrationRecord.Record.ProtocolVersion > MAX_PROTOCOL_VERSION)) { this.logger.LogDebug("Registration protocol version out of bounds " + tx.GetHash()); continue; } // If there were other registrations for this server previously, remove them and add the new one this.registrationStore.AddWithReplace(registrationRecord); this.logger.LogDebug("Registration transaction for server collateral address: " + registrationRecord.Record.ServerId); this.logger.LogDebug("Server Onion address: " + registrationRecord.Record.OnionAddress); this.logger.LogDebug("Server configuration hash: " + registrationRecord.Record.ConfigurationHash); // Add collateral address to watch only wallet so that any funding transactions can be detected this.watchOnlyWalletManager.WatchAddress(registrationRecord.Record.ServerId); } catch (Exception e) { this.logger.LogDebug("Failed to parse registration transaction, exception: " + e); } } } WatchOnlyWallet watchOnlyWallet = this.watchOnlyWalletManager.GetWatchOnlyWallet(); // TODO: Need to have 'current height' field in watch-only wallet so that we don't start rebalancing collateral balances before the latest block has been processed & incorporated // Perform watch-only wallet housekeeping - iterate through known servers foreach (RegistrationRecord record in this.registrationStore.GetAll()) { Script scriptToCheck = BitcoinAddress.Create(record.Record.ServerId, this.network).ScriptPubKey; this.logger.LogDebug("Recalculating collateral balance for server: " + record.Record.ServerId); if (!watchOnlyWallet.WatchedAddresses.ContainsKey(scriptToCheck.ToString())) { this.logger.LogDebug("Server address missing from watch-only wallet. Deleting stored registrations for server: " + record.Record.ServerId); this.registrationStore.DeleteAllForServer(record.Record.ServerId); continue; } Money serverCollateralBalance = this.watchOnlyWalletManager.GetRelativeBalance(scriptToCheck.ToString()); this.logger.LogDebug("Collateral balance for server " + record.Record.ServerId + " is " + serverCollateralBalance.ToString() + ", original registration height " + record.BlockReceived + ", current height " + height); if ((serverCollateralBalance < MASTERNODE_COLLATERAL_THRESHOLD) && ((height - record.BlockReceived) > WINDOW_PERIOD_BLOCK_COUNT)) { // Remove server registrations as funding has not been performed timeously, // or funds have been removed from the collateral address subsequent to the // registration being performed this.logger.LogDebug("Insufficient collateral within window period for server: " + record.Record.ServerId); this.logger.LogDebug("Deleting registration records for server: " + record.Record.ServerId); this.registrationStore.DeleteAllForServer(record.Record.ServerId); // TODO: Remove unneeded transactions from the watch-only wallet? // TODO: Need to make the TumbleBitFeature change its server address if this is the address it was using } } this.watchOnlyWalletManager.SaveWatchOnlyWallet(); } }
/// <inheritdoc /> public void ProcessBlock(int height, Block block) { // Check for any server registration transactions if (block.Transactions != null) { foreach (Transaction tx in block.Transactions) { // Check if the transaction has the Breeze registration marker output (literal text BREEZE_REGISTRATION_MARKER) if (tx.Outputs[0].ScriptPubKey.ToHex().ToLower() == "6a1a425245455a455f524547495354524154494f4e5f4d41524b4552") { this.logger.LogDebug("Received a new registration transaction: " + tx.GetHash()); try { RegistrationToken registrationToken = new RegistrationToken(); registrationToken.ParseTransaction(tx, this.network); if (!registrationToken.Validate(this.network)) { continue; } MerkleBlock merkleBlock = new MerkleBlock(block, new uint256[] { tx.GetHash() }); RegistrationRecord registrationRecord = new RegistrationRecord(DateTime.Now, Guid.NewGuid(), tx.GetHash().ToString(), tx.ToHex(), registrationToken, merkleBlock.PartialMerkleTree, height); // Ignore protocol versions outside the accepted bounds if (registrationRecord.Record.ProtocolVersion < MIN_PROTOCOL_VERSION) { continue; } if (registrationRecord.Record.ProtocolVersion > MAX_PROTOCOL_VERSION) { continue; } // If there were other registrations for this server previously, remove them and add the new one this.registrationStore.AddWithReplace(registrationRecord); this.logger.LogDebug("Registration transaction for server collateral address: " + registrationRecord.Record.ServerId); this.logger.LogDebug("Server Onion address: " + registrationRecord.Record.OnionAddress); this.logger.LogDebug("Server configuration hash: " + registrationRecord.Record.ConfigurationHash); this.watchOnlyWalletManager.WatchAddress(registrationRecord.Record.ServerId); } catch (Exception e) { this.logger.LogDebug("Failed to parse registration transaction " + tx.GetHash() + ", exception: " + e); } } } WatchOnlyWallet watchOnlyWallet = this.watchOnlyWalletManager.GetWatchOnlyWallet(); // TODO: Need to have 'current height' field in watch-only wallet so that we don't start rebalancing collateral balances before the latest block has been processed & incorporated // Perform watch-only wallet housekeeping - iterate through known servers HashSet <string> knownServers = new HashSet <string>(); // TODO: This is very CPU intensive and slows down the initial sync. // TODO: Incorporate a cache of server balances and a dirty flag on transaction receipt to trigger rebalance? foreach (RegistrationRecord record in this.registrationStore.GetAll()) { if (knownServers.Add(record.Record.ServerId)) { this.logger.LogDebug("Calculating collateral balance for server: " + record.Record.ServerId); //var addrToCheck = new WatchedAddress //{ // Script = BitcoinAddress.Create(record.Record.ServerId, this.network).ScriptPubKey, // Address = record.Record.ServerId //}; var scriptToCheck = BitcoinAddress.Create(record.Record.ServerId, this.network).ScriptPubKey; if (!watchOnlyWallet.WatchedAddresses.ContainsKey(scriptToCheck.ToString())) { this.logger.LogDebug("Server address missing from watch-only wallet. Deleting stored registrations for server: " + record.Record.ServerId); this.registrationStore.DeleteAllForServer(record.Record.ServerId); continue; } // Initially the server's balance is zero Money serverCollateralBalance = new Money(0); // Need this for looking up other transactions in the watch-only wallet TransactionData prevTransaction; // TODO: Move balance evaluation logic into helper methods in watch-only wallet itself // Now evaluate the watch-only balance for this server foreach (string txId in watchOnlyWallet.WatchedAddresses[scriptToCheck.ToString()].Transactions.Keys) { TransactionData transaction = watchOnlyWallet.WatchedAddresses[scriptToCheck.ToString()].Transactions[txId]; // First check if the inputs contain the watched address foreach (TxIn input in transaction.Transaction.Inputs) { // See if we have the previous transaction in our watch-only wallet. watchOnlyWallet.WatchedAddresses[scriptToCheck.ToString()].Transactions.TryGetValue(input.PrevOut.Hash.ToString(), out prevTransaction); // If it is null, it can't be related to one of the watched addresses (or it is the very first watched transaction) if (prevTransaction == null) { continue; } if (prevTransaction.Transaction.Outputs[input.PrevOut.N].ScriptPubKey == scriptToCheck) { // Input = funds are being paid out of the address in question // Computing the input value is a bit more complex than it looks, as the value is not directly stored // in a TxIn object. We need to check the output being spent by the input to get this information. // But even an OutPoint does not contain the Value - we need to check the other transactions in the // watch-only wallet to see if we have the prior transaction being referenced. // This does imply that the earliest transaction in the watch-only wallet (for this address) will not // have full previous transaction information stored. Therefore we can only reason about the address // balance after a given block height; any prior transactions are ignored. serverCollateralBalance -= prevTransaction.Transaction.Outputs[input.PrevOut.N].Value; } } // Check if the outputs contain the watched address foreach (var output in transaction.Transaction.Outputs) { //if (output.ScriptPubKey.GetScriptAddress(this.network).ToString() == record.Record.ServerId) if (output.ScriptPubKey == scriptToCheck) { // Output = funds are being paid into the address in question serverCollateralBalance += output.Value; } } } this.logger.LogDebug("Collateral balance for server " + record.Record.ServerId + " is " + serverCollateralBalance.ToString() + ", original registration height " + record.BlockReceived + " current height " + height); // Ignore collateral requirements for protocol version 252, just in case if ((serverCollateralBalance < MASTERNODE_COLLATERAL_THRESHOLD) && ((height - record.BlockReceived) > WINDOW_PERIOD_BLOCK_COUNT)) { // Remove server registrations this.logger.LogDebug("Insufficient collateral within window period for server: " + record.Record.ServerId); this.logger.LogDebug("Deleting registration records for server: " + record.Record.ServerId); this.registrationStore.DeleteAllForServer(record.Record.ServerId); // TODO: Remove unneeded transactions from the watch-only wallet? // TODO: Need to make the TumbleBitFeature change its server address if this is the address it was using } } } this.watchOnlyWalletManager.SaveWatchOnlyWallet(); } }