Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dashpay): add timeskew support to CoinJoinService and warning dialogs #1252

Merged
merged 6 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions wallet/res/values/strings-extra.xml
Original file line number Diff line number Diff line change
Expand Up @@ -697,4 +697,6 @@
<string name="masternode_key_not_used">Not used</string>
<string name="masternode_key_revoked">Revoked</string>
<string name="masternode_key_private_public_base64">Private / Public Keys (base64)</string>
<string name="timeskew_ahead">ahead</string>
<string name="timeskew_behind">behind</string>
</resources>
2 changes: 2 additions & 0 deletions wallet/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
<string name="wallet_low_storage_dialog_button_apps">Manage apps</string>
<string name="wallet_timeskew_dialog_title">Check date &amp; time settings</string>
<string name="wallet_timeskew_dialog_msg">Your device time is off by %d minutes. You probably cannot send or receive Dash due to this problem.\n\nYou should check and if necessary correct your date, time and timezone settings.</string>
<string name="wallet_coinjoin_timeskew_dialog_msg">Your device time is %s by %d seconds. You cannot use CoinJoin due to this difference.\n\nThe time settings on your device needs to be changed to “Set time automatically” to use CoinJoin.</string>
<string name="settings_coinjoin_timeskew_dialog_msg">Your device time is off by more than 5 seconds. You cannot use CoinJoin due to this difference.\n\nThe time settings on your device needs to be changed to “Set time automatically” before using CoinJoin.</string>
<string name="send_coins_activity_title">Send Dash</string>
<string name="send_coins_fragment_request_payment_request_progress">Fetching signature from %s…</string>
<string name="send_coins_fragment_request_payment_request_failed_title">Fetching signature failed</string>
Expand Down
82 changes: 68 additions & 14 deletions wallet/src/de/schildbach/wallet/service/CoinJoinService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,15 @@

package de.schildbach.wallet.service

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
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
Expand Down Expand Up @@ -50,7 +56,6 @@ import org.bitcoinj.coinjoin.utils.CoinJoinManager
import org.bitcoinj.coinjoin.utils.CoinJoinTransactionType
import org.bitcoinj.core.AbstractBlockChain
import org.bitcoinj.core.Coin
import org.bitcoinj.core.Context
import org.bitcoinj.core.ECKey
import org.bitcoinj.core.MasternodeAddress
import org.bitcoinj.core.Transaction
Expand Down Expand Up @@ -84,6 +89,7 @@ interface CoinJoinService {
suspend fun getMixingState(): MixingStatus
fun observeMixingState(): Flow<MixingStatus>
fun observeMixingProgress(): Flow<Double>
fun updateTimeSkew(timeSkew: Long)
}

enum class MixingStatus {
Expand All @@ -94,8 +100,12 @@ enum class MixingStatus {
ERROR // An error stopped the mixing process
}

const val MAX_ALLOWED_AHEAD_TIMESKEW = 5000L // 5 seconds
const val MAX_ALLOWED_BEHIND_TIMESKEW = 20000L // 20 seconds

@Singleton
class CoinJoinMixingService @Inject constructor(
@ApplicationContext private val context: Context,
val walletDataProvider: WalletDataProvider,
private val blockchainStateProvider: BlockchainStateProvider,
private val config: CoinJoinConfig,
Expand All @@ -111,7 +121,15 @@ class CoinJoinMixingService @Inject constructor(
const val DEFAULT_DENOMINATIONS_HARDCAP = 300

// these are not for production
val FAST_MIXING_DENOMINATIONS_REMOVE = listOf<Denomination>() // Denomination.THOUSANDTH)
val FAST_MIXING_DENOMINATIONS_REMOVE = listOf<Denomination>() // Denomination.THOUSANDTH

fun isInsideTimeSkewBounds(timeSkew: Long): Boolean {
return if (timeSkew > 0) {
timeSkew < MAX_ALLOWED_AHEAD_TIMESKEW
} else {
(-timeSkew) < MAX_ALLOWED_BEHIND_TIMESKEW
}
}
}

private val coinJoinManager: CoinJoinManager?
Expand Down Expand Up @@ -140,12 +158,25 @@ class CoinJoinMixingService @Inject constructor(
private var networkStatus: NetworkStatus = NetworkStatus.UNKNOWN
private var isSynced = false
private var hasAnonymizableBalance: Boolean = false
private var timeSkew: Long = 0L

// https://stackoverflow.com/questions/55421710/how-to-suspend-kotlin-coroutine-until-notified
private val updateMutex = Mutex(locked = false)
private val updateMixingStateMutex = Mutex(locked = false)
private var exception: Throwable? = null

private val timeChangeReceiver = object: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == Intent.ACTION_TIME_CHANGED) {
// Time has changed, handle the change here
log.info("Time or Time Zone changed")
coroutineScope.launch {
updateTimeSkewInternal(getTimeSkew())
}
}
}
}

override fun observeMixingProgress(): Flow<Double> = _progressFlow

init {
Expand All @@ -168,7 +199,7 @@ class CoinJoinMixingService @Inject constructor(
.onEach { blockChainState ->
val isSynced = blockChainState.isSynced()
if (isSynced != this.isSynced) {
updateState(config.getMode(), hasAnonymizableBalance, networkStatus, isSynced, blockChain)
updateState(config.getMode(), timeSkew, hasAnonymizableBalance, networkStatus, isSynced, blockChain)
}
// this will trigger mixing as new blocks are mined and received tx's are confirmed
if (isSynced) {
Expand Down Expand Up @@ -196,9 +227,18 @@ class CoinJoinMixingService @Inject constructor(
.launchIn(coroutineScope)
}

/** 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)
}

private suspend fun updateBalance(balance: Coin) {
// leave this ui scope
Context.propagate(walletDataProvider.wallet!!.context)
CoinJoinClientOptions.setAmount(balance)
log.info("coinjoin: total balance: ${balance.toFriendlyString()}")
val walletEx = walletDataProvider.wallet as WalletEx
Expand All @@ -223,9 +263,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(),
getTimeSkew(),
hasBalanceLeftToMix,
networkStatus,
isSynced,
Expand All @@ -235,26 +280,29 @@ class CoinJoinMixingService @Inject constructor(

private suspend fun updateState(
mode: CoinJoinMode,
timeSkew: Long,
hasAnonymizableBalance: Boolean,
networkStatus: NetworkStatus,
isSynced: Boolean,
blockChain: AbstractBlockChain?
) {
updateMutex.lock()
log.info(
"coinjoin-old-state: ${this.mode}, ${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, $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
this.mode = mode
this.timeSkew = timeSkew

if (mode == CoinJoinMode.NONE) {
if (mode == CoinJoinMode.NONE || !isInsideTimeSkewBounds(timeSkew)) {
updateMixingState(MixingStatus.NOT_STARTED)
} else {
configureMixing()
Expand Down Expand Up @@ -302,11 +350,11 @@ class CoinJoinMixingService @Inject constructor(
}

private suspend fun updateBlockChain(blockChain: AbstractBlockChain?) {
updateState(mode, hasAnonymizableBalance, networkStatus, isSynced, blockChain)
updateState(mode, timeSkew, hasAnonymizableBalance, networkStatus, isSynced, blockChain)
}

private suspend fun updateNetworkStatus(networkStatus: NetworkStatus) {
updateState(mode, hasAnonymizableBalance, networkStatus, isSynced, blockChain)
updateState(mode, timeSkew, hasAnonymizableBalance, networkStatus, isSynced, blockChain)
}

private suspend fun updateMode(mode: CoinJoinMode) {
Expand All @@ -315,7 +363,8 @@ class CoinJoinMixingService @Inject constructor(
configureMixing()
updateBalance(walletDataProvider.wallet!!.getBalance(Wallet.BalanceType.AVAILABLE))
}
updateState(mode, hasAnonymizableBalance, networkStatus, isSynced, blockChain)
val currentTimeSkew = getTimeSkew()
updateState(mode, currentTimeSkew, hasAnonymizableBalance, networkStatus, isSynced, blockChain)
}

private var mixingProgressTracker: MixingProgressTracker = object : MixingProgressTracker() {
Expand Down Expand Up @@ -475,15 +524,19 @@ class CoinJoinMixingService @Inject constructor(
}

private suspend fun startMixing(): Boolean {
Context.propagate(walletDataProvider.wallet!!.context)
val filter = IntentFilter().apply {
addAction(Intent.ACTION_TIME_CHANGED)
}
context.registerReceiver(timeChangeReceiver, filter)
clientManager.setBlockChain(blockChain)
return if (!clientManager.startMixing()) {
log.info("Mixing has been started already.")
false
} else {
// run this on a different thread?
val asyncStart = coroutineScope.async(Dispatchers.IO) {
Context.propagate(walletDataProvider.wallet!!.context)
// 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()
}
Expand Down Expand Up @@ -511,6 +564,7 @@ class CoinJoinMixingService @Inject constructor(
sessionCompleteListeners.forEach { coinJoinManager?.removeSessionCompleteListener(it) }
clientManager.stopMixing()
coinJoinManager?.stop()
context.unregisterReceiver(timeChangeReceiver)
}

private fun setBlockchain(blockChain: AbstractBlockChain?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@

package de.schildbach.wallet.ui.coinjoin

import android.content.Intent
import android.os.Bundle
import android.provider.Settings
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
Expand Down Expand Up @@ -81,7 +83,7 @@ class CoinJoinLevelFragment : Fragment(R.layout.fragment_coinjoin_level) {
}
}

private fun showConnectionWaringDialog(mode: CoinJoinMode) {
private fun showConnectionWarningDialog(mode: CoinJoinMode) {
AdaptiveDialog.create(
R.drawable.ic_warning,
getString(
Expand Down Expand Up @@ -123,9 +125,27 @@ class CoinJoinLevelFragment : Fragment(R.layout.fragment_coinjoin_level) {
}
}
} else if (viewModel.isWifiConnected()) {
setMode(mode)
val settingsIntent = Intent(Settings.ACTION_DATE_SETTINGS)
val hasSettings = requireActivity().packageManager.resolveActivity(settingsIntent, 0) != null
lifecycleScope.launch {
if (viewModel.isTimeSkewedForCoinJoin()) {
AdaptiveDialog.create(
R.drawable.ic_coinjoin,
getString(R.string.coinjoin),
getString(R.string.settings_coinjoin_timeskew_dialog_msg),
getString(R.string.cancel),
if (hasSettings) getString(R.string.button_settings) else null
).show(requireActivity()) { openSettings ->
if (openSettings == true && hasSettings) {
startActivity(settingsIntent)
}
}
} else {
setMode(mode)
}
}
} else {
showConnectionWaringDialog(mode)
showConnectionWarningDialog(mode)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import de.schildbach.wallet.data.CoinJoinConfig
import de.schildbach.wallet.service.CoinJoinMode
import de.schildbach.wallet.service.CoinJoinService
import de.schildbach.wallet.service.MAX_ALLOWED_AHEAD_TIMESKEW
import de.schildbach.wallet.service.MAX_ALLOWED_BEHIND_TIMESKEW
import de.schildbach.wallet.util.getTimeSkew
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filterNotNull
Expand All @@ -35,6 +39,7 @@ import javax.inject.Inject
open class CoinJoinLevelViewModel @Inject constructor(
private val analytics: AnalyticsService,
private val coinJoinConfig: CoinJoinConfig,
private val coinJoinService: CoinJoinService,
private var networkState: NetworkStateInt
) : ViewModel() {

Expand Down Expand Up @@ -64,4 +69,18 @@ open class CoinJoinLevelViewModel @Inject constructor(
fun logEvent(event: String) {
analytics.logEvent(event, mapOf())
}

suspend fun isTimeSkewedForCoinJoin(): Boolean {
return try {
val timeSkew = getTimeSkew()
coinJoinService.updateTimeSkew(timeSkew)
if (timeSkew > 0) {
timeSkew > MAX_ALLOWED_AHEAD_TIMESKEW
} else {
-timeSkew > MAX_ALLOWED_BEHIND_TIMESKEW
}
} catch (e: Exception) {
false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ class CreateIdentityService : LifecycleService() {
}

private val walletApplication by lazy { application as WalletApplication }
@Inject lateinit var platformRepo: PlatformRepo// by lazy { PlatformRepo.getInstance() }
@Inject lateinit var platformRepo: PlatformRepo
@Inject lateinit var platformSyncService: PlatformSyncService
@Inject lateinit var userAlertDao: UserAlertDao
@Inject lateinit var blockchainIdentityDataDao: BlockchainIdentityConfig
Expand Down
18 changes: 8 additions & 10 deletions wallet/src/de/schildbach/wallet/ui/main/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package de.schildbach.wallet.ui.main

import android.Manifest
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
Expand Down Expand Up @@ -50,6 +51,7 @@ import de.schildbach.wallet.ui.dashpay.*
import de.schildbach.wallet.ui.invite.AcceptInviteActivity
import de.schildbach.wallet.ui.invite.InviteHandler
import de.schildbach.wallet.ui.invite.InviteSendContactRequestDialog
import de.schildbach.wallet.ui.main.WalletActivityExt.checkLowStorageAlert
import de.schildbach.wallet.ui.main.WalletActivityExt.checkTimeSkew
import de.schildbach.wallet.ui.main.WalletActivityExt.handleFirebaseAction
import de.schildbach.wallet.ui.main.WalletActivityExt.requestDisableBatteryOptimisation
Expand Down Expand Up @@ -109,7 +111,7 @@ class MainActivity : AbstractBindServiceActivity(), ActivityCompat.OnRequestPerm

val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { _ ->
requestDisableBatteryOptimisation()
};
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand All @@ -129,9 +131,9 @@ class MainActivity : AbstractBindServiceActivity(), ActivityCompat.OnRequestPerm

handleIntent(intent)

//Prevent showing dialog twice or more when activity is recreated (e.g: rotating device, etc)
// Prevent showing dialog twice or more when activity is recreated (e.g: rotating device, etc)
if (savedInstanceState == null) {
//Add BIP44 support and PIN if missing
// Add BIP44 support and PIN if missing
upgradeWalletKeyChains(Constants.BIP44_PATH, false)
}

Expand All @@ -143,6 +145,9 @@ class MainActivity : AbstractBindServiceActivity(), ActivityCompat.OnRequestPerm
currencies.component1()!!, currencies.component2()!!
)
}
val timeChangedFilter = IntentFilter().apply {
addAction(Intent.ACTION_TIME_CHANGED)
}
}

override fun onStart() {
Expand Down Expand Up @@ -532,13 +537,6 @@ class MainActivity : AbstractBindServiceActivity(), ActivityCompat.OnRequestPerm
}
}

private fun checkLowStorageAlert() {
val stickyIntent = registerReceiver(null, IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW))
if (stickyIntent != null) {
showLowStorageAlertDialog()
}
}
Comment on lines -535 to -540
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DashPay was using the old checkLowStorageAlert()


override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
Expand Down
Loading
Loading