From c904b1413fde55b0d35ae6868162bed87fad0b90 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Mon, 12 Feb 2024 13:43:10 -0800 Subject: [PATCH] fix: consolidate timeSkew functions and link better into CoinJoinService --- .../wallet/service/CoinJoinService.kt | 51 ++++++++-------- .../schildbach/wallet/ui/main/MainActivity.kt | 11 ---- .../wallet/ui/main/MainViewModel.kt | 12 ++-- .../wallet/ui/main/WalletActivityExt.kt | 3 +- .../de/schildbach/wallet/util/TimeUtils.kt | 58 +++++++++++++++++++ 5 files changed, 88 insertions(+), 47 deletions(-) create mode 100644 wallet/src/de/schildbach/wallet/util/TimeUtils.kt diff --git a/wallet/src/de/schildbach/wallet/service/CoinJoinService.kt b/wallet/src/de/schildbach/wallet/service/CoinJoinService.kt index 5e8d77233b..9f14917ed8 100644 --- a/wallet/src/de/schildbach/wallet/service/CoinJoinService.kt +++ b/wallet/src/de/schildbach/wallet/service/CoinJoinService.kt @@ -25,6 +25,7 @@ import com.google.common.base.Stopwatch import dagger.hilt.android.qualifiers.ApplicationContext import de.schildbach.wallet.data.CoinJoinConfig import de.schildbach.wallet.ui.dashpay.PlatformRepo +import de.schildbach.wallet.util.getTimeSkew import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asCoroutineDispatcher @@ -66,15 +67,12 @@ import org.bouncycastle.crypto.params.KeyParameter import org.dash.wallet.common.WalletDataProvider import org.dash.wallet.common.data.NetworkStatus import org.dash.wallet.common.services.BlockchainStateProvider -import org.dash.wallet.common.util.Constants -import org.dash.wallet.common.util.head import org.slf4j.Logger import org.slf4j.LoggerFactory import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton -import kotlin.math.abs enum class CoinJoinMode { NONE, @@ -91,6 +89,7 @@ interface CoinJoinService { suspend fun getMixingState(): MixingStatus fun observeMixingState(): Flow fun observeMixingProgress(): Flow + fun updateTimeSkew(timeSkew: Long) } enum class MixingStatus { @@ -117,7 +116,7 @@ class CoinJoinMixingService @Inject constructor( const val DEFAULT_SESSIONS = 4 const val DEFAULT_DENOMINATIONS_GOAL = 50 const val DEFAULT_DENOMINATIONS_HARDCAP = 300 - const val MAX_ALLOWED_TIMESKEW = 2000L // 2 seconds + const val MAX_ALLOWED_TIMESKEW = 4000L // 4 seconds // these are not for production val FAST_MIXING_DENOMINATIONS_REMOVE = listOf() // Denomination.THOUSANDTH) @@ -162,7 +161,7 @@ class CoinJoinMixingService @Inject constructor( // Time has changed, handle the change here log.info("Time or Time Zone changed") coroutineScope.launch { - updateTimeSkew(getTimeSkew()) + updateTimeSkewInternal(getTimeSkew()) } } } @@ -218,7 +217,14 @@ class CoinJoinMixingService @Inject constructor( .launchIn(coroutineScope) } - private suspend fun updateTimeSkew(timeSkew: Long) { + /** updates timeSkew in #[coroutineScope] */ + override fun updateTimeSkew(timeSkew: Long) { + coroutineScope.launch { + updateTimeSkewInternal(timeSkew) + } + } + + suspend fun updateTimeSkewInternal(timeSkew: Long) { updateState(config.getMode(), timeSkew, hasAnonymizableBalance, networkStatus, isSynced, blockChain) } @@ -247,10 +253,14 @@ class CoinJoinMixingService @Inject constructor( else -> false } - log.info("coinjoin: mixing can occur: $hasBalanceLeftToMix = balance: (${anonBalance.isGreaterThan(CoinJoin.getSmallestDenomination())} && canDenominate: $canDenominate) || partially-mixed: $hasPartiallyMixedCoins") + log.info( + "coinjoin: mixing can occur: $hasBalanceLeftToMix = balance: (${anonBalance.isGreaterThan( + CoinJoin.getSmallestDenomination() + )} && canDenominate: $canDenominate) || partially-mixed: $hasPartiallyMixedCoins" + ) updateState( config.getMode(), - timeSkew, + getTimeSkew(), hasBalanceLeftToMix, networkStatus, isSynced, @@ -268,13 +278,14 @@ class CoinJoinMixingService @Inject constructor( ) { updateMutex.lock() log.info( - "coinjoin-old-state: ${this.mode}, ${this.timeSkew / 1000}s, ${this.hasAnonymizableBalance}, ${this.networkStatus}, synced: ${this.isSynced} ${blockChain != null}" + "coinjoin-old-state: ${this.mode}, ${this.timeSkew}ms, ${this.hasAnonymizableBalance}, ${this.networkStatus}, synced: ${this.isSynced} ${blockChain != null}" ) try { setBlockchain(blockChain) log.info( - "coinjoin-new-state: $mode, ${timeSkew / 1000}s, $hasAnonymizableBalance, $networkStatus, synced: $isSynced, ${blockChain != null}" + "coinjoin-new-state: $mode, $timeSkew ms, $hasAnonymizableBalance, $networkStatus, synced: $isSynced, ${blockChain != null}" ) + log.info("coinjoin-Current timeskew: ${getTimeSkew()}") this.networkStatus = networkStatus this.hasAnonymizableBalance = hasAnonymizableBalance this.isSynced = isSynced @@ -336,24 +347,6 @@ class CoinJoinMixingService @Inject constructor( updateState(mode, timeSkew, hasAnonymizableBalance, networkStatus, isSynced, blockChain) } - private suspend fun getTimeSkew(): Long { - val systemTimeMillis = System.currentTimeMillis() - var result = Constants.HTTP_CLIENT.head("https://www.dash.org/") - var networkTime = result.headers.getDate("date")?.time - if (networkTime == null) { - result = Constants.HTTP_CLIENT.head("https://insight.dash.org/insight") - networkTime = result.headers.getDate("date")?.time - } - requireNotNull(networkTime) - if (networkStatus == NetworkStatus.CONNECTED) { - val peerList = walletDataProvider.wallet!!.context.peerGroup.connectedPeers - peerList.forEach { peer -> - peer.lastPingTime - } - } - return abs(systemTimeMillis - networkTime) - } - private suspend fun updateMode(mode: CoinJoinMode) { CoinJoinClientOptions.setEnabled(mode != CoinJoinMode.NONE) if (mode != CoinJoinMode.NONE && this.mode == CoinJoinMode.NONE) { @@ -532,6 +525,8 @@ class CoinJoinMixingService @Inject constructor( } else { // run this on a different thread? val asyncStart = coroutineScope.async(Dispatchers.IO) { + // though coroutineScope is on a Context propogated thread, we still need this + org.bitcoinj.core.Context.propagate(walletDataProvider.wallet!!.context) coinJoinManager?.initMasternodeGroup(blockChain) clientManager.doAutomaticDenominating() } diff --git a/wallet/src/de/schildbach/wallet/ui/main/MainActivity.kt b/wallet/src/de/schildbach/wallet/ui/main/MainActivity.kt index 89082af7b1..50291f725a 100644 --- a/wallet/src/de/schildbach/wallet/ui/main/MainActivity.kt +++ b/wallet/src/de/schildbach/wallet/ui/main/MainActivity.kt @@ -113,15 +113,6 @@ class MainActivity : AbstractBindServiceActivity(), ActivityCompat.OnRequestPerm requestDisableBatteryOptimisation() } - private val timeChangedReceiver = object: BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - if (intent?.action == Intent.ACTION_TIME_CHANGED) { - // Time has changed, handle the change here - checkTimeSkew(viewModel) - } - } - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) @@ -157,7 +148,6 @@ class MainActivity : AbstractBindServiceActivity(), ActivityCompat.OnRequestPerm val timeChangedFilter = IntentFilter().apply { addAction(Intent.ACTION_TIME_CHANGED) } - registerReceiver(timeChangedReceiver, timeChangedFilter) } override fun onStart() { @@ -571,7 +561,6 @@ class MainActivity : AbstractBindServiceActivity(), ActivityCompat.OnRequestPerm override fun onDestroy() { super.onDestroy() viewModel.platformRepo.onIdentityResolved = null - unregisterReceiver(timeChangedReceiver) } override fun onLockScreenDeactivated() { diff --git a/wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt b/wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt index 24f1a5e405..73f72bdf9d 100644 --- a/wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt +++ b/wallet/src/de/schildbach/wallet/ui/main/MainViewModel.kt @@ -56,6 +56,7 @@ import de.schildbach.wallet.ui.dashpay.PlatformRepo import de.schildbach.wallet.ui.dashpay.utils.DashPayConfig import de.schildbach.wallet.ui.dashpay.work.SendContactRequestOperation import de.schildbach.wallet.ui.transactions.TransactionRowView +import de.schildbach.wallet.util.getTimeSkew import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -136,8 +137,8 @@ class MainViewModel @Inject constructor( companion object { private const val THROTTLE_DURATION = 500L private const val DIRECTION_KEY = "tx_direction" - private const val TIME_SKEW_TOLERANCE = 3600 // seconds (1 hour) - private const val TIME_SKEW_TOLERANCE_COINJOIN = 2 // seconds + private const val TIME_SKEW_TOLERANCE = 3600000 // seconds (1 hour) + private const val TIME_SKEW_TOLERANCE_COINJOIN = 4000 // seconds private val log = LoggerFactory.getLogger(MainViewModel::class.java) } @@ -387,16 +388,13 @@ class MainViewModel @Inject constructor( suspend fun getDeviceTimeSkew(): Pair { return try { - val systemTimeMillis = System.currentTimeMillis() - val result = HTTP_CLIENT.head("https://www.dash.org/") - val networkTime = result.headers.getDate("date")?.time - requireNotNull(networkTime) + val timeSkew = getTimeSkew() val maxAllowedTimeSkew = if (coinJoinConfig.getMode() == CoinJoinMode.NONE) { TIME_SKEW_TOLERANCE } else { TIME_SKEW_TOLERANCE_COINJOIN } - val timeSkew = abs(systemTimeMillis - networkTime) / 1000 + coinJoinService.updateTimeSkew(timeSkew) return Pair(timeSkew > maxAllowedTimeSkew, timeSkew) } catch (ex: Exception) { // Ignore errors diff --git a/wallet/src/de/schildbach/wallet/ui/main/WalletActivityExt.kt b/wallet/src/de/schildbach/wallet/ui/main/WalletActivityExt.kt index ef4d20cb04..301e5b4d67 100644 --- a/wallet/src/de/schildbach/wallet/ui/main/WalletActivityExt.kt +++ b/wallet/src/de/schildbach/wallet/ui/main/WalletActivityExt.kt @@ -100,7 +100,8 @@ object WalletActivityExt { val coinJoinOn = viewModel.getCoinJoinMode() != CoinJoinMode.NONE if (isTimeSkewed && (!timeSkewDialogShown || coinJoinOn)) { timeSkewDialogShown = true - showTimeSkewAlertDialog(timeSkew, coinJoinOn) + // add 1 to round up so 2.2 seconds appears as 3 + showTimeSkewAlertDialog(1 + timeSkew / 1000, coinJoinOn) } } } diff --git a/wallet/src/de/schildbach/wallet/util/TimeUtils.kt b/wallet/src/de/schildbach/wallet/util/TimeUtils.kt new file mode 100644 index 0000000000..4a9a7fef87 --- /dev/null +++ b/wallet/src/de/schildbach/wallet/util/TimeUtils.kt @@ -0,0 +1,58 @@ +package de.schildbach.wallet.util + +import org.dash.wallet.common.util.Constants +import org.dash.wallet.common.util.head +import java.net.DatagramPacket +import java.net.DatagramSocket +import java.net.InetAddress +import kotlin.math.abs + +private fun queryNtpTime(server: String): Long? { + try { + val address = InetAddress.getByName(server) + val message = ByteArray(48) + message[0] = 0b00100011 // NTP mode client (3) and version (4) + + val socket = DatagramSocket().apply { + soTimeout = 3000 // Set timeout to 3000ms + } + + val request = DatagramPacket(message, message.size, address, 123) + socket.send(request) // Send request + + // Receive response + val response = DatagramPacket(message, message.size) + socket.receive(response) + + // Timestamp starts at byte 40 of the received packet and is four bytes, + // or two words, long. First byte is the high-order byte of the integer; + // the last byte is the low-order byte. The high word is the seconds field, + // and the low word is the fractional field. + val seconds = message[40].toLong() and 0xff shl 24 or + (message[41].toLong() and 0xff shl 16) or + (message[42].toLong() and 0xff shl 8) or + (message[43].toLong() and 0xff) + + // Convert seconds to milliseconds and adjust from 1900 to epoch (1970) + return (seconds - 2208988800L) * 1000 + } catch (e: Exception) { + e.printStackTrace() + } + return null +} + +suspend fun getTimeSkew(): Long { + var networkTime = queryNtpTime("pool.ntp.org") + if (networkTime == null) { + var result = Constants.HTTP_CLIENT.head("https://www.dash.org/") + + networkTime = result.headers.getDate("date")?.time + if (networkTime == null) { + result = Constants.HTTP_CLIENT.head("https://insight.dash.org/insight") + networkTime = result.headers.getDate("date")?.time + } + requireNotNull(networkTime) + } + val systemTimeMillis = System.currentTimeMillis() + return abs(systemTimeMillis - networkTime) +}