public void RegistrationStoreAddTest() { var privateKey = new Key(); var token = new RegistrationToken(255, IPAddress.Parse("127.0.0.1"), IPAddress.Parse("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), "", 37123, new KeyId("dbb476190a81120928763ee8ce97e4c0bcfd6624"), new KeyId("dbb476190a81120928763ee8ce97e4c0bcfd6624"), privateKey.PubKey, new Uri("https://redstone.com/test")); token.Signature = CryptoUtils.SignData(token.GetHeaderBytes().ToArray(), privateKey); RegistrationRecord record = new RegistrationRecord(DateTime.Now, Guid.NewGuid(), "137cc93a23383252348de58e53e193db471099e78ef89edf1fdea6ff340a7261", "0100000027c16c59051604707fcfc75e7ab33decbe72e72baff44ad2b38b78a13929eb5d092df004b20200000048473044022067c3c50f13a1b68402549c6a049a9298d8de03e176909cc6b44f9fd9d9db532502203e7416233a125264aef46698b22f429f60be9ed3b2115ec24f0a05de2a4e4cd901feffffff1d92cb2776b3c3cb3e89ad84c38c72fc72646238d23312a6b1931e7980f90f590200000049483045022100d4141e26371a198fb22347e0cc64fd0563ee11640108836e5f427873e4aa0d00022012960447890e151b46a19c2d1d480f7e81e5ceb615c9566be03189108cb08ce401feffffffa23c4e0da07acb439735e6f2a3c7a1ee15521138cc3815fdbac428e1756a3976010000004948304502210083dfda6e8a7584741c79d60a8c69c372789eb28680fdf43fe15e67eb05c7628e022041cde42e79e83c5be5902c035abcde02579e1841473efbbeb5a909d4520a8ea801fefffffff3b56cf319b4b4895beae1e047d77fddcfd8b86a3964226c0d24dda83335e40802000000484730440220219623b855cd1ac0ad43cb3b76097cb52db2d37b4a6a768a5911dfa3571ff4b602203eb843072f746e225e6c7225438f4f4bf012c18f1a1572d4ead288190d8d61cb01feffffff15209f56f6f5d1246f43057ab546be192659a3c88365529667eb3aa364ed4392000000006b483045022100a5791707155d03fb6e770c0dd0924cf08a3bd3b6e0b5713d7d3e84dc5c2e18bc02200370afce15733f5139a018d3ffc5e594e646ce372274818d9780906f9fb3699101210344e875df3990bf55d7218020b09aea6e1383206ec88344847771b3bc0d72251bfeffffff0220188fba000000001976a914b88f742a0a07af27ccfe21de8a40b9f7541f3e0088ac00e87648170000001976a914db0be998354d2139b14e06459d295de03b94fadb88ac90920000", token, null); RegistrationStore store = new RegistrationStore(Path.GetTempFileName()); Assert.True(store.Add(record)); }
public void RegistrationStoreGetOneTest() { var token = new RegistrationToken(255, "1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2", IPAddress.Parse("127.0.0.1"), IPAddress.Parse("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), "0123456789ABCDEF", 37123); token.RsaSignature = Encoding.ASCII.GetBytes("xyz"); token.EcdsaSignature = Encoding.ASCII.GetBytes("abc"); RegistrationRecord record = new RegistrationRecord(DateTime.Now, Guid.NewGuid(), "137cc93a23383252348de58e53e193db471099e78ef89edf1fdea6ff340a7261", "0100000027c16c59051604707fcfc75e7ab33decbe72e72baff44ad2b38b78a13929eb5d092df004b20200000048473044022067c3c50f13a1b68402549c6a049a9298d8de03e176909cc6b44f9fd9d9db532502203e7416233a125264aef46698b22f429f60be9ed3b2115ec24f0a05de2a4e4cd901feffffff1d92cb2776b3c3cb3e89ad84c38c72fc72646238d23312a6b1931e7980f90f590200000049483045022100d4141e26371a198fb22347e0cc64fd0563ee11640108836e5f427873e4aa0d00022012960447890e151b46a19c2d1d480f7e81e5ceb615c9566be03189108cb08ce401feffffffa23c4e0da07acb439735e6f2a3c7a1ee15521138cc3815fdbac428e1756a3976010000004948304502210083dfda6e8a7584741c79d60a8c69c372789eb28680fdf43fe15e67eb05c7628e022041cde42e79e83c5be5902c035abcde02579e1841473efbbeb5a909d4520a8ea801fefffffff3b56cf319b4b4895beae1e047d77fddcfd8b86a3964226c0d24dda83335e40802000000484730440220219623b855cd1ac0ad43cb3b76097cb52db2d37b4a6a768a5911dfa3571ff4b602203eb843072f746e225e6c7225438f4f4bf012c18f1a1572d4ead288190d8d61cb01feffffff15209f56f6f5d1246f43057ab546be192659a3c88365529667eb3aa364ed4392000000006b483045022100a5791707155d03fb6e770c0dd0924cf08a3bd3b6e0b5713d7d3e84dc5c2e18bc02200370afce15733f5139a018d3ffc5e594e646ce372274818d9780906f9fb3699101210344e875df3990bf55d7218020b09aea6e1383206ec88344847771b3bc0d72251bfeffffff0220188fba000000001976a914b88f742a0a07af27ccfe21de8a40b9f7541f3e0088ac00e87648170000001976a914db0be998354d2139b14e06459d295de03b94fadb88ac90920000", token); RegistrationStore store = new RegistrationStore(Path.GetTempFileName()); store.Add(record); var retrievedRecords = store.GetAll(); Assert.Equal(retrievedRecords.Count, 1); var retrievedRecord = retrievedRecords[0].Record; Assert.Equal(retrievedRecord.ProtocolVersion, 255); Assert.Equal(retrievedRecord.ServerId, "1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2"); Assert.Equal(retrievedRecord.Ipv4Addr, IPAddress.Parse("127.0.0.1")); Assert.Equal(retrievedRecord.Ipv6Addr, IPAddress.Parse("2001:0db8:85a3:0000:0000:8a2e:0370:7334")); Assert.Equal(retrievedRecord.OnionAddress, "0123456789ABCDEF"); Assert.Equal(retrievedRecord.Port, 37123); }
public void Register(object tag, Action<int> callback) { var rr = new RegistrationRecord(); rr.tag = tag; rr.callback = callback; Unregister(tag); records.Add(rr); OnHooksChanged(); }
public void Register(object tag, Action <int> callback) { var rr = new RegistrationRecord(); rr.tag = tag; rr.callback = callback; Unregister(tag); records.Add(rr); OnHooksChanged(); }
public bool Delete(RegistrationRecord record) { try { Delete(record.RecordGuid); } catch (Exception) { return(false); } return(true); }
/// <summary> /// Checks if the server ID in the supplied record already exists in the store, /// and removes all other records before adding. /// </summary> /// <param name="regRecord"></param> /// <returns></returns> public bool AddWithReplace(RegistrationRecord regRecord) { foreach (RegistrationRecord record in GetByServerId(regRecord.Token.ServerId)) { Delete(record.RecordGuid); } lock (lock_object) { List <RegistrationRecord> registrations = GetRecordsOrCreateFile(); registrations.Add(regRecord); string regJson = JsonConvert.SerializeObject(registrations); File.WriteAllText(this.StorePath, regJson); return(true); } }
/// <inheritdoc /> public async Task <Result <ClassicTumblerParameters> > ChangeServerAsync() { // First stop the state machine if applicable if (this.stateMachine != null && this.stateMachine.Started) { await this.stateMachine.Stop().ConfigureAwait(false); } this.State = TumbleState.OnlyMonitor; // Now select a different masternode List <RegistrationRecord> registrations = this.registrationStore.GetAll(); if (registrations.Count < MINIMUM_MASTERNODE_COUNT) { this.logger.LogDebug("Not enough masternode registrations downloaded yet: " + registrations.Count); return(Result.Fail <ClassicTumblerParameters>("Not enough masternode registrations downloaded yet")); } registrations.Shuffle(); // Since the list is shuffled, we can simply try the first one in the list. // Unlike the connect method, we only try one server here. That is because // a timeout can take in the order of minutes for each server tried. RegistrationRecord record = registrations.First(); this.TumblerAddress = "ctb://" + record.Record.OnionAddress + ".onion?h=" + record.Record.ConfigurationHash; var attemptConnection = await TryUseServer(); if (!attemptConnection.Failure) { return(attemptConnection); } // If we reach this point, the server was unreachable this.logger.LogDebug("Failed to connect to server, try another"); return(Result.Fail <ClassicTumblerParameters>("Failed to connect to server, try another")); }
public bool Add(RegistrationRecord regRecord) { lock (lock_object) { List <RegistrationRecord> registrations = GetRecordsOrCreateFile(); registrations.Add(regRecord); //JsonSerializerSettings settings = new JsonSerializerSettings(); //settings.Converters.Add(new IPAddressConverter()); //settings.Formatting = Formatting.Indented; //JsonSerializerSettings isoDateFormatSettings = new JsonSerializerSettings //{ // DateFormatHandling = DateFormatHandling.IsoDateFormat //}; string regJson = JsonConvert.SerializeObject(registrations); File.WriteAllText(this.StorePath, regJson); return(true); } }
// TODO: how do we deal with re-orgs? private void ScanForServiceNodeRegistrations(int height, Block block) { foreach (Transaction tx in block.Transactions.Where(RegistrationToken.HasMarker)) { this.logger.LogDebug("Received a new service node registration transaction: " + tx.GetHash()); try { var registrationToken = new RegistrationToken(); registrationToken.ParseTransaction(tx, this.network); if (!registrationToken.Validate(this.network)) { this.logger.LogDebug("Registration token failed validation"); continue; } // TODO: doesn't belong here var merkleBlock = new MerkleBlock(block, new[] { tx.GetHash() }); var registrationRecord = new RegistrationRecord(DateTime.Now, Guid.NewGuid(), tx.GetHash().ToString(), tx.ToHex(), registrationToken, merkleBlock.PartialMerkleTree, height); string collateralAddress = registrationRecord.Token.CollateralPubKeyHash.GetAddress(this.network).ToString(); this.logger.LogTrace("New Service Node Registration"); var serviceNode = new Redstone.ServiceNode.Models.ServiceNode(registrationRecord); this.serviceNodeManager.AddServiceNode(serviceNode); } catch (Exception e) { this.logger.LogDebug("Failed to parse registration transaction, exception: " + e); } } this.serviceNodeManager.SyncedHeight = height; this.serviceNodeManager.SyncedBlockHash = block.GetHash(); }
/// <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 async Task <Result <ClassicTumblerParameters> > ConnectToTumblerAsync() { // Temporary hardcoding for testnet if (this.TumblerAddress == null) { this.TumblerAddress = "ctb://6cvi6ulcd4qn42mi.onion?h=95cb936fde9ae1856bcfd953746c26724a25dc46"; } // If the -ppuri command line option wasn't used to bypass the registration store lookup if (this.TumblerAddress == null) { List <RegistrationRecord> registrations = this.registrationStore.GetAll(); if (registrations.Count < MINIMUM_MASTERNODE_COUNT) { this.logger.LogDebug("Not enough masternode registrations downloaded yet: " + registrations.Count); return(null); } RegistrationRecord record = null; RegistrationToken registrationToken = null; bool validRegistrationFound = false; // TODO: Search the registration store more robustly for (int i = 1; i < 10; i++) { record = registrations[random.Next(registrations.Count)]; registrationToken = record.Record; // Implicitly, the registration feature will have deleted the registration if the collateral // requirement was not met within 30 blocks if ((this.walletManager.LastBlockHeight() - record.BlockReceived) >= 32) { validRegistrationFound = true; break; } } if (!validRegistrationFound) { this.logger.LogDebug("Did not find a valid registration"); return(Result.Fail <ClassicTumblerParameters>("Did not find a valid registration")); } this.TumblerAddress = "ctb://" + registrationToken.OnionAddress + ".onion?h=" + registrationToken.ConfigurationHash; } this.tumblingState.TumblerUri = new Uri(this.TumblerAddress); FullNodeTumblerClientConfiguration config; if (this.TumblerAddress.Contains("127.0.0.1")) { config = new FullNodeTumblerClientConfiguration(this.tumblingState, onlyMonitor: false, connectionTest: true, useProxy: false); } else { config = new FullNodeTumblerClientConfiguration(this.tumblingState, onlyMonitor: false, connectionTest: true, useProxy: true); } TumblerClientRuntime rt = null; try { rt = await TumblerClientRuntime.FromConfigurationAsync(config, connectionTest : true) .ConfigureAwait(false); // This is overwritten by the tumble method, but it is needed at the beginning of that method for the balance check this.TumblerParameters = rt.TumblerParameters; return(Result.Ok(rt.TumblerParameters)); } catch (Exception cex) when(cex is PrivacyProtocolConfigException || cex is ConfigException) { this.logger.LogError("Error obtaining tumbler parameters: " + cex); return(Result.Fail <ClassicTumblerParameters>( cex is PrivacyProtocolConfigException ? "Tor is required for connectivity to an active Stratis Masternode. Please restart Breeze Wallet with Privacy Protocol and ensure that an instance of Tor is running." : cex.Message)); } catch (Exception e) { this.logger.LogError("Error obtaining tumbler parameters: " + e); return(Result.Fail <ClassicTumblerParameters>("Error obtaining tumbler parameters")); } finally { rt?.Dispose(); } }
public bool CheckBreezeRegistration(BreezeConfiguration config, string regStorePath, string configurationHash, string onionAddress, RsaKey tumblerKey) { // In order to determine if the registration sequence has been performed // before, and to see if a previous performance is still valid, interrogate // the database to see if any transactions have been recorded. RegistrationStore regStore = new RegistrationStore(regStorePath); List <RegistrationRecord> transactions = regStore.GetByServerId(config.TumblerEcdsaKeyAddress); // If no transactions exist, the registration definitely needs to be done if (transactions == null || transactions.Count == 0) { return(false); } RegistrationRecord mostRecent = null; foreach (RegistrationRecord record in transactions) { // Find most recent transaction if (mostRecent == null) { mostRecent = record; } if (record.RecordTimestamp > mostRecent.RecordTimestamp) { mostRecent = record; } } // Check if the stored record matches the current configuration RegistrationToken registrationToken; try { registrationToken = mostRecent.Record; } catch (NullReferenceException e) { Console.WriteLine(e); return(false); } // IPv4 if (config.Ipv4Address == null && registrationToken.Ipv4Addr != null) { return(false); } if (config.Ipv4Address != null && registrationToken.Ipv4Addr == null) { return(false); } if (config.Ipv4Address != null && registrationToken.Ipv4Addr != null) { if (!config.Ipv4Address.Equals(registrationToken.Ipv4Addr)) { return(false); } } // IPv6 if (config.Ipv6Address == null && registrationToken.Ipv6Addr != null) { return(false); } if (config.Ipv6Address != null && registrationToken.Ipv6Addr == null) { return(false); } if (config.Ipv6Address != null && registrationToken.Ipv6Addr != null) { if (!config.Ipv6Address.Equals(registrationToken.Ipv6Addr)) { return(false); } } // Onion if (onionAddress != registrationToken.OnionAddress) { return(false); } if (config.Port != registrationToken.Port) { return(false); } // This verifies that the tumbler parameters are unchanged if (configurationHash != registrationToken.ConfigurationHash) { return(false); } // TODO: Check if transaction is actually confirmed on the blockchain? return(true); }
public Transaction PerformBreezeRegistration(BreezeConfiguration config, string regStorePath, string configurationHash, string onionAddress, RsaKey tumblerKey) { Network network = Network.ImpleumMain; if (config.TumblerNetwork == Network.TestNet || config.TumblerNetwork == Network.RegTest) { network = Network.ImpleumTest; } RPCHelper stratisHelper = null; RPCClient stratisRpc = null; BitcoinSecret privateKeyEcdsa = null; try { stratisHelper = new RPCHelper(network); stratisRpc = stratisHelper.GetClient(config.RpcUser, config.RpcPassword, config.RpcUrl); privateKeyEcdsa = stratisRpc.DumpPrivKey(BitcoinAddress.Create(config.TumblerEcdsaKeyAddress)); } catch (Exception e) { Console.WriteLine("ERROR: Unable to retrieve private key to fund registration transaction"); Console.WriteLine("Is the Stratis wallet unlocked & RPC enabled?"); Console.WriteLine(e); Environment.Exit(0); } RegistrationToken registrationToken = new RegistrationToken(PROTOCOL_VERSION_TO_USE, config.TumblerEcdsaKeyAddress, config.Ipv4Address, config.Ipv6Address, onionAddress, configurationHash, config.Port, privateKeyEcdsa.PubKey); byte[] msgBytes = registrationToken.GetRegistrationTokenBytes(tumblerKey, privateKeyEcdsa); // Create the registration transaction using the bytes generated above Transaction rawTx = CreateBreezeRegistrationTx(network, msgBytes, config.TxOutputValueSetting); TransactionUtils txUtils = new TransactionUtils(); RegistrationStore regStore = new RegistrationStore(regStorePath); try { // Replace fundrawtransaction with C# implementation. The legacy wallet // software does not support the RPC call. Transaction fundedTx = txUtils.FundRawTx(stratisRpc, rawTx, config.TxFeeValueSetting, BitcoinAddress.Create(config.TumblerEcdsaKeyAddress)); RPCResponse signedTx = stratisRpc.SendCommand("signrawtransaction", fundedTx.ToHex()); Transaction txToSend = new Transaction(((JObject)signedTx.Result)["hex"].Value <string>()); RegistrationRecord regRecord = new RegistrationRecord(DateTime.Now, Guid.NewGuid(), txToSend.GetHash().ToString(), txToSend.ToHex(), registrationToken, null); regStore.Add(regRecord); stratisRpc.SendRawTransaction(txToSend); return(txToSend); } catch (Exception e) { Console.WriteLine("ERROR: Unable to broadcast registration transaction"); Console.WriteLine(e); } return(null); }
/// <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(); } }
/// <inheritdoc /> public async Task <ClassicTumblerParameters> ConnectToTumblerAsync() { if (this.network == Network.TestNet) { this.TumblerAddress = "ctb://sz64kj6ev5576w34.onion?h=ceced829426faf63cb906b99e5ee1ff83f001a95"; } else { this.TumblerAddress = "ctb://frspe6yz6en4wbrt.onion?h=14dad7205ff3632f5ab903b9052116397bf7302f"; } // If the -ppuri command line option wasn't used to bypass the registration store lookup if (this.TumblerAddress == null) { List <RegistrationRecord> registrations = this.registrationStore.GetAll(); if (registrations.Count < MINIMUM_MASTERNODE_COUNT) { this.logger.LogDebug("Not enough masternode registrations downloaded yet: " + registrations.Count); return(null); } RegistrationRecord record = null; RegistrationToken registrationToken = null; bool validRegistrationFound = false; // TODO: Search the registration store more robustly for (int i = 1; i < 10; i++) { record = registrations[random.Next(registrations.Count)]; registrationToken = record.Record; // Implicitly, the registration feature will have deleted the registration if the collateral // requirement was not met within 30 blocks if ((this.walletManager.LastBlockHeight() - record.BlockReceived) >= 32) { validRegistrationFound = true; break; } } if (!validRegistrationFound) { this.logger.LogDebug("Did not find a valid registration"); return(null); } this.TumblerAddress = "ctb://" + registrationToken.OnionAddress + ".onion?h=" + registrationToken.ConfigurationHash; } this.tumblingState.TumblerUri = new Uri(this.TumblerAddress); var config = new FullNodeTumblerClientConfiguration(this.tumblingState, onlyMonitor: false, connectionTest: true); TumblerClientRuntime rt = null; try { rt = await TumblerClientRuntime.FromConfigurationAsync(config, connectionTest : true).ConfigureAwait(false); // This is overwritten by the tumble method, but it is needed at the beginning of that method for the balance check this.TumblerParameters = rt.TumblerParameters; return(rt.TumblerParameters); } catch (Exception e) { this.logger.LogError("Error obtaining tumbler parameters: " + e); return(null); } finally { rt?.Dispose(); } }
/// <inheritdoc /> public async Task <Result <ClassicTumblerParameters> > ConnectToTumblerAsync(HashSet <string> masternodeBlacklist = null) { // Assumptions about the current state coming into this method: // - If this is a first connection, this.TumblerAddress will be null // - If we were previously connected to a server, its URI would have been stored in the // tumbling_state.json, and will have been loaded into this.TumblerAddress already List <RegistrationRecord> registrations = this.GetValidRegistrations(); if (this.TumblerAddress == null) { if (registrations.Count < MINIMUM_MASTERNODE_COUNT) { this.logger.LogDebug($"Not enough masternode registrations downloaded yet: {registrations.Count}"); return(Result.Fail <ClassicTumblerParameters>("Not enough masternode registrations downloaded yet", PostResultActionType.CanContinue)); } registrations.Shuffle(); // Since the list is shuffled, we can simply iterate through it and try each server until one is valid & reachable. foreach (RegistrationRecord record in registrations) { if (record.Record.IsDummyAddress) { this.TumblerAddress = $"ctb://{record.Record.Ipv4Addr}:{record.Record.Port}?h={record.Record.ConfigurationHash}"; } else { this.TumblerAddress = $"ctb://{record.Record.OnionAddress}.onion?h={record.Record.ConfigurationHash}"; } //Do not attempt a connection to the Masternode which is blacklisted if (masternodeBlacklist != null && masternodeBlacklist.Contains(this.TumblerAddress)) { this.logger.LogDebug($"Skipping connection attempt to blacklisted masternode {this.TumblerAddress}"); continue; } //Overwrite tumbler address for regtest to indicate to the user that it it not a legimimate masternode if (record.Record.IsDummyAddress && UseDummyAddress) { this.TumblerDisplayAddress = $"ctb://{record.Record.OnionAddress}.dummy?h={record.Record.ConfigurationHash}"; } else { this.TumblerDisplayAddress = this.TumblerAddress; } var tumblerParameterResult = await TryUseServer(); if (tumblerParameterResult.Success) { return(tumblerParameterResult); } else if (tumblerParameterResult.PostResultAction == PostResultActionType.ShouldStop) { return(tumblerParameterResult); } } this.logger.LogDebug($"Attempted connection to {registrations.Count} masternodes and did not find a valid registration"); return(Result.Fail <ClassicTumblerParameters>("Did not find a valid registration", PostResultActionType.ShouldStop)); } else { Result <ClassicTumblerParameters> tumblerParameterResult = await TryUseServer(); Uri tumblerAddressUri = new Uri(this.TumblerAddress); RegistrationRecord record = registrations.FirstOrDefault(r => r.Record.Ipv4Addr != null && r.Record.Ipv4Addr.ToString() == tumblerAddressUri.DnsSafeHost); if (record != null) { //Overwrite tumbler address for regtest to indicate to the user that it it not a legimimate masternode if (record.Record.IsDummyAddress && UseDummyAddress) { this.TumblerDisplayAddress = $"ctb://{record.Record.OnionAddress}.dummy?h={record.Record.ConfigurationHash}"; } else { this.TumblerDisplayAddress = this.TumblerAddress; } } tumblerParameterResult = await TryUseServer(); if (tumblerParameterResult.Success) { return(tumblerParameterResult); } // Blacklist masternode address which we have just failed to connect to so that // we won't attempt to connect to it again in the next call to ConnectToTumblerAsync. HashSet <string> blacklistedMasternodes = new HashSet <string>() { this.TumblerAddress }; // The masternode that was being used in a previous run is now unreachable. // Restart the connection process and try to find a working server. this.TumblerAddress = null; return(await ConnectToTumblerAsync(blacklistedMasternodes)); } }