support rolling backup of multisig wallets

This commit is contained in:
woodser 2022-06-28 10:34:20 -04:00
parent f17c5803b5
commit c28108a4b6
4 changed files with 45 additions and 21 deletions

View file

@ -18,7 +18,7 @@
package bisq.common.crypto; package bisq.common.crypto;
import bisq.common.config.Config; import bisq.common.config.Config;
import bisq.common.file.FileUtil;
import com.google.inject.Inject; import com.google.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
@ -139,6 +139,7 @@ public class KeyStorage {
* @param secretKey The symmetric key that protects the key entry file * @param secretKey The symmetric key that protects the key entry file
*/ */
public KeyPair loadKeyPair(KeyEntry keyEntry, SecretKey secretKey) { public KeyPair loadKeyPair(KeyEntry keyEntry, SecretKey secretKey) {
FileUtil.rollingBackup(storageDir, keyEntry.getFileName() + ".key", 20);
try { try {
KeyFactory keyFactory = KeyFactory.getInstance(keyEntry.getAlgorithm()); KeyFactory keyFactory = KeyFactory.getInstance(keyEntry.getAlgorithm());
byte[] encodedPrivateKey = loadKeyBytes(keyEntry, secretKey); byte[] encodedPrivateKey = loadKeyBytes(keyEntry, secretKey);

View file

@ -74,6 +74,19 @@ public class FileUtil {
} }
} }
public static void deleteRollingBackup(File dir, String fileName) {
File backupDir = new File(Paths.get(dir.getAbsolutePath(), "backup").toString());
if (!backupDir.exists()) throw new RuntimeException("backup directory does not exist: " + backupDir);
String dirName = "backups_" + fileName;
if (dirName.contains(".")) dirName = dirName.replace(".", "_");
File backupFileDir = new File(Paths.get(backupDir.getAbsolutePath(), dirName).toString());
try {
FileUtils.deleteDirectory(backupFileDir);
} catch (IOException e) {
e.printStackTrace();
}
}
private static void pruneBackup(File backupDir, int numMaxBackupFiles) { private static void pruneBackup(File backupDir, int numMaxBackupFiles) {
if (backupDir.isDirectory()) { if (backupDir.isDirectory()) {
File[] files = backupDir.listFiles(); File[] files = backupDir.listFiles();

View file

@ -76,6 +76,7 @@ public class XmrWalletService {
private static final String MONERO_WALLET_RPC_USERNAME = "haveno_user"; private static final String MONERO_WALLET_RPC_USERNAME = "haveno_user";
private static final String MONERO_WALLET_RPC_DEFAULT_PASSWORD = "password"; // only used if account password is null private static final String MONERO_WALLET_RPC_DEFAULT_PASSWORD = "password"; // only used if account password is null
private static final String MONERO_WALLET_NAME = "haveno_XMR"; private static final String MONERO_WALLET_NAME = "haveno_XMR";
private static final String MONERO_MULTISIG_WALLET_PREFIX = "xmr_multisig_trade_";
private static final long MONERO_WALLET_SYNC_PERIOD = 5000l; private static final long MONERO_WALLET_SYNC_PERIOD = 5000l;
private final CoreAccountService accountService; private final CoreAccountService accountService;
@ -168,7 +169,7 @@ public class XmrWalletService {
} }
public boolean multisigWalletExists(String tradeId) { public boolean multisigWalletExists(String tradeId) {
return walletExists("xmr_multisig_trade_" + tradeId); return walletExists(MONERO_MULTISIG_WALLET_PREFIX + tradeId);
} }
// TODO (woodser): test retaking failed trade. create new multisig wallet or replace? cannot reuse // TODO (woodser): test retaking failed trade. create new multisig wallet or replace? cannot reuse
@ -177,7 +178,7 @@ public class XmrWalletService {
Trade trade = tradeManager.getOpenTrade(tradeId).get(); Trade trade = tradeManager.getOpenTrade(tradeId).get();
synchronized (trade) { synchronized (trade) {
if (multisigWallets.containsKey(trade.getId())) return multisigWallets.get(trade.getId()); if (multisigWallets.containsKey(trade.getId())) return multisigWallets.get(trade.getId());
String path = "xmr_multisig_trade_" + trade.getId(); String path = MONERO_MULTISIG_WALLET_PREFIX + trade.getId();
MoneroWallet multisigWallet = createWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null); // auto-assign port MoneroWallet multisigWallet = createWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null); // auto-assign port
multisigWallets.put(trade.getId(), multisigWallet); multisigWallets.put(trade.getId(), multisigWallet);
return multisigWallet; return multisigWallet;
@ -190,7 +191,7 @@ public class XmrWalletService {
Trade trade = tradeManager.getTrade(tradeId); Trade trade = tradeManager.getTrade(tradeId);
synchronized (trade) { synchronized (trade) {
if (multisigWallets.containsKey(trade.getId())) return multisigWallets.get(trade.getId()); if (multisigWallets.containsKey(trade.getId())) return multisigWallets.get(trade.getId());
String path = "xmr_multisig_trade_" + trade.getId(); String path = MONERO_MULTISIG_WALLET_PREFIX + trade.getId();
if (!walletExists(path)) throw new RuntimeException("Multisig wallet does not exist for trade " + trade.getId()); if (!walletExists(path)) throw new RuntimeException("Multisig wallet does not exist for trade " + trade.getId());
MoneroWallet multisigWallet = openWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null); MoneroWallet multisigWallet = openWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null);
multisigWallets.put(trade.getId(), multisigWallet); multisigWallets.put(trade.getId(), multisigWallet);
@ -198,6 +199,11 @@ public class XmrWalletService {
} }
} }
public void saveWallet(MoneroWallet wallet) {
wallet.save();
backupWallet(wallet.getPath());
}
public void closeMultisigWallet(String tradeId) { public void closeMultisigWallet(String tradeId) {
log.info("{}.closeMultisigWallet({})", getClass().getSimpleName(), tradeId); log.info("{}.closeMultisigWallet({})", getClass().getSimpleName(), tradeId);
Trade trade = tradeManager.getTrade(tradeId); Trade trade = tradeManager.getTrade(tradeId);
@ -212,7 +218,7 @@ public class XmrWalletService {
log.info("{}.deleteMultisigWallet({})", getClass().getSimpleName(), tradeId); log.info("{}.deleteMultisigWallet({})", getClass().getSimpleName(), tradeId);
Trade trade = tradeManager.getTrade(tradeId); Trade trade = tradeManager.getTrade(tradeId);
synchronized (trade) { synchronized (trade) {
String walletName = "xmr_multisig_trade_" + tradeId; String walletName = MONERO_MULTISIG_WALLET_PREFIX + tradeId;
if (!walletExists(walletName)) return false; if (!walletExists(walletName)) return false;
if (multisigWallets.containsKey(trade.getId())) closeMultisigWallet(tradeId); if (multisigWallets.containsKey(trade.getId())) closeMultisigWallet(tradeId);
deleteWallet(walletName); deleteWallet(walletName);
@ -372,9 +378,6 @@ public class XmrWalletService {
private void initialize() { private void initialize() {
// backup wallet files
backupWallets();
// initialize main wallet if connected or previously created // initialize main wallet if connected or previously created
tryInitMainWallet(); tryInitMainWallet();
@ -402,7 +405,7 @@ public class XmrWalletService {
try { try {
wallet.sync(); // blocking wallet.sync(); // blocking
connectionsService.doneDownload(); // TODO: using this to signify both daemon and wallet synced, refactor sync handling of both connectionsService.doneDownload(); // TODO: using this to signify both daemon and wallet synced, refactor sync handling of both
wallet.save(); saveWallet(wallet);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
@ -485,12 +488,6 @@ public class XmrWalletService {
return MONERO_WALLET_RPC_MANAGER.startInstance(cmd); return MONERO_WALLET_RPC_MANAGER.startInstance(cmd);
} }
private void backupWallets() {
FileUtil.rollingBackup(walletDir, xmrWalletFile.getName(), 20);
FileUtil.rollingBackup(walletDir, xmrWalletFile.getName() + ".keys", 20);
FileUtil.rollingBackup(walletDir, xmrWalletFile.getName() + ".address.txt", 20);
}
private void setWalletDaemonConnections(MoneroRpcConnection connection) { private void setWalletDaemonConnections(MoneroRpcConnection connection) {
log.info("Setting wallet daemon connections: " + (connection == null ? null : connection.getUri())); log.info("Setting wallet daemon connections: " + (connection == null ? null : connection.getUri()));
if (wallet == null) tryInitMainWallet(); if (wallet == null) tryInitMainWallet();
@ -520,7 +517,7 @@ public class XmrWalletService {
public void run() { public void run() {
try { try {
wallet.changePassword(oldPassword, newPassword); wallet.changePassword(oldPassword, newPassword);
wallet.save(); saveWallet(wallet);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
throw e; throw e;
@ -534,7 +531,7 @@ public class XmrWalletService {
MoneroWallet multisigWallet = getMultisigWallet(tradeId); // TODO (woodser): this unnecessarily connects and syncs unopen wallets and leaves open MoneroWallet multisigWallet = getMultisigWallet(tradeId); // TODO (woodser): this unnecessarily connects and syncs unopen wallets and leaves open
if (multisigWallet == null) return; if (multisigWallet == null) return;
multisigWallet.changePassword(oldPassword, newPassword); multisigWallet.changePassword(oldPassword, newPassword);
multisigWallet.save(); saveWallet(multisigWallet);
} }
}); });
} }
@ -552,7 +549,8 @@ public class XmrWalletService {
log.info("{}.closeWallet({}, {})", getClass().getSimpleName(), walletRpc.getPath(), save); log.info("{}.closeWallet({}, {})", getClass().getSimpleName(), walletRpc.getPath(), save);
MoneroError err = null; MoneroError err = null;
try { try {
walletRpc.close(save); if (save) saveWallet(walletRpc);
walletRpc.close();
} catch (MoneroError e) { } catch (MoneroError e) {
err = e; err = e;
} }
@ -567,7 +565,7 @@ public class XmrWalletService {
if (!new File(path).delete()) throw new RuntimeException("Failed to delete wallet file: " + path); if (!new File(path).delete()) throw new RuntimeException("Failed to delete wallet file: " + path);
if (!new File(path + ".keys").delete()) throw new RuntimeException("Failed to delete wallet file: " + path); if (!new File(path + ".keys").delete()) throw new RuntimeException("Failed to delete wallet file: " + path);
if (!new File(path + ".address.txt").delete()) throw new RuntimeException("Failed to delete wallet file: " + path); if (!new File(path + ".address.txt").delete()) throw new RuntimeException("Failed to delete wallet file: " + path);
// WalletsSetup.deleteRollingBackup(walletName); // TODO (woodser): necessary to delete rolling backup? deleteBackupWallets(walletName);
} }
private void closeAllWallets() { private void closeAllWallets() {
@ -609,6 +607,18 @@ public class XmrWalletService {
multisigWallets.clear(); multisigWallets.clear();
} }
private void backupWallet(String walletName) {
FileUtil.rollingBackup(walletDir, walletName, 20);
FileUtil.rollingBackup(walletDir, walletName + ".keys", 20);
FileUtil.rollingBackup(walletDir, walletName + ".address.txt", 20);
}
private void deleteBackupWallets(String walletName) {
FileUtil.deleteRollingBackup(walletDir, walletName);
FileUtil.deleteRollingBackup(walletDir, walletName + ".keys");
FileUtil.deleteRollingBackup(walletDir, walletName + ".address.txt");
}
// ----------------------------- LEGACY APP ------------------------------- // ----------------------------- LEGACY APP -------------------------------
public XmrAddressEntry recoverAddressEntry(String offerId, String address, XmrAddressEntry.Context context) { public XmrAddressEntry recoverAddressEntry(String offerId, String address, XmrAddressEntry.Context context) {

View file

@ -124,7 +124,7 @@ public class SendSignContractRequestAfterMultisig extends TradeTask {
} }
private void completeAux() { private void completeAux() {
processModel.getXmrWalletService().getWallet().save(); processModel.getXmrWalletService().saveWallet(processModel.getXmrWalletService().getWallet());
complete(); complete();
} }
} }