package co.gem.round.coinop; import co.gem.round.encoding.Base58; import co.gem.round.encoding.Hex; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import org.bitcoinj.core.*; import org.bitcoinj.crypto.DeterministicKey; import org.bitcoinj.crypto.HDKeyDerivation; import org.bitcoinj.crypto.TransactionSignature; import org.bitcoinj.script.Script; import org.bitcoinj.script.ScriptBuilder; import org.bitcoinj.wallet.DeterministicKeyChain; import org.bitcoinj.wallet.DeterministicSeed; import java.nio.ByteBuffer; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class MultiWallet { public static enum Blockchain { TESTNET, MAINNET } private byte[] primarySeed, backupSeed; private DeterministicKey primaryPrivateKey, backupPrivateKey, backupPublicKey, cosignerPublicKey; private NetworkParameters networkParameters; private MultiWallet(NetworkParameters networkParameters) { this.networkParameters = networkParameters; SecureRandom random = new SecureRandom(); this.primarySeed = new DeterministicKeyChain(random).getSeed().getSeedBytes(); this.backupSeed = new DeterministicKeyChain(random).getSeed().getSeedBytes(); this.primaryPrivateKey = HDKeyDerivation.createMasterPrivateKey(primarySeed); this.backupPrivateKey = HDKeyDerivation.createMasterPrivateKey(backupSeed); this.backupPublicKey = HDKeyDerivation.createMasterPubKeyFromBytes(backupPrivateKey.getPubKey(), backupPrivateKey.getChainCode()); } private MultiWallet(String primaryPrivateSeed, String backupPublicKey, String cosignerPublicKey) { byte[] decodedCosignerPublicKey = new byte[0]; byte[] decodedBackupPublicKey = new byte[0]; byte[] decodedPrimarySeed = new byte[DeterministicSeed.DEFAULT_SEED_ENTROPY_BITS/8]; try { decodedCosignerPublicKey = Base58.decode(cosignerPublicKey); decodedBackupPublicKey = Base58.decode(backupPublicKey); decodedPrimarySeed = Hex.decode(primaryPrivateSeed); } catch (Exception e) { e.printStackTrace(); } this.primaryPrivateKey = HDKeyDerivation.createMasterPrivateKey(decodedPrimarySeed); if (backupPublicKey != null) { ByteBuffer buffer = ByteBuffer.wrap(decodedBackupPublicKey); NetworkParameters networkParameters = networkParametersFromHeaderBytes(buffer.getInt()); this.backupPublicKey = DeterministicKey.deserializeB58(backupPublicKey, networkParameters); } if (cosignerPublicKey != null) { ByteBuffer buffer = ByteBuffer.wrap(decodedCosignerPublicKey); NetworkParameters networkParameters = networkParametersFromHeaderBytes(buffer.getInt()); this.cosignerPublicKey = DeterministicKey.deserializeB58(cosignerPublicKey, networkParameters); } this.networkParameters = networkParametersFromBlockchain(Blockchain.MAINNET); } public static NetworkParameters networkParametersFromBlockchain(Blockchain blockchain) { switch (blockchain) { case MAINNET: return NetworkParameters.fromID(NetworkParameters.ID_MAINNET); case TESTNET: return NetworkParameters.fromID(NetworkParameters.ID_TESTNET); } return NetworkParameters.fromID(NetworkParameters.ID_TESTNET); } public static NetworkParameters networkParametersFromHeaderBytes(int headerBytes) { if (headerBytes == 0x043587CF || headerBytes == 0x04358394) return NetworkParameters.fromID(NetworkParameters.ID_TESTNET); if (headerBytes == 0x0488B21E || headerBytes == 0x0488ADE4) return NetworkParameters.fromID(NetworkParameters.ID_MAINNET); return NetworkParameters.fromID(NetworkParameters.ID_TESTNET); } public static MultiWallet generate(Blockchain blockchain) { NetworkParameters networkParameters = networkParametersFromBlockchain(blockchain); return new MultiWallet(networkParameters); } public static MultiWallet importSeeds(String primaryPrivateSeed, String backupPublicSeed, String cosignerPublicSeed) { return new MultiWallet(primaryPrivateSeed, backupPublicSeed, cosignerPublicSeed); } public Blockchain blockchain() { if (networkParameters.getId().equals(NetworkParameters.ID_MAINNET)) return Blockchain.MAINNET; else if (networkParameters.getId().equals(NetworkParameters.ID_TESTNET)) return Blockchain.TESTNET; return Blockchain.TESTNET; } public String serializedPrimaryPrivateSeed() { return Hex.encode(this.primarySeed); } public String serializedBackupPrivateSeed() { return Hex.encode(this.backupSeed); } public String serializedPrimaryPrivateKey() { return this.primaryPrivateKey.serializePrivB58(networkParameters); } public String serializedPrimaryPublicKey() { return this.primaryPrivateKey.serializePubB58(networkParameters); } public String serializedBackupPrivateKey() { return this.backupPrivateKey.serializePrivB58(networkParameters); } public String serializedBackupPublicKey() { return this.backupPublicKey.serializePubB58(networkParameters); } public String serializedCosignerPublicKey() { return this.cosignerPublicKey.serializePubB58(networkParameters); } public void purgeSeeds() { this.primarySeed = null; this.backupSeed = null; } public DeterministicKey childPrimaryPrivateKeyFromPath(String path) { return childKeyFromPath(path, this.primaryPrivateKey); } public DeterministicKey childPrimaryPublicKeyFromPath(String path) { return childKeyFromPath(path, this.primaryPrivateKey.dropPrivateBytes().dropParent()); } public DeterministicKey childBackupPublicKeyFromPath(String path) { return childKeyFromPath(path, this.backupPublicKey); } public DeterministicKey childCosignerPublicKeyFromPath(String path) { return childKeyFromPath(path, this.cosignerPublicKey); } public static DeterministicKey childKeyFromPath(String path, DeterministicKey parentKey) { String[] segments = path.split("/"); DeterministicKey currentKey = parentKey; for (int i = 1; i < segments.length; i++) { int childNumber = Integer.parseInt(segments[i]); currentKey = HDKeyDerivation.deriveChildKey(currentKey, childNumber); } return currentKey; } public Script redeemScriptForPath(String path) { DeterministicKey primaryPublicKey = this.childPrimaryPublicKeyFromPath(path); DeterministicKey backupPublicKey = this.childBackupPublicKeyFromPath(path); DeterministicKey cosignerPublicKey = this.childCosignerPublicKeyFromPath(path); List<ECKey> pubKeys = Arrays.asList(new ECKey[]{ backupPublicKey, cosignerPublicKey, primaryPublicKey}); return ScriptBuilder.createMultiSigOutputScript(2, pubKeys); } public String base58SignatureForPath(String walletPath, Sha256Hash sigHash) { DeterministicKey primaryPrivateKey = this.childPrimaryPrivateKeyFromPath(walletPath); TransactionSignature signature = new TransactionSignature(primaryPrivateKey.sign(sigHash), Transaction.SigHash.ALL, false); return Base58.encode(signature.encodeToBitcoin()); } public NetworkParameters networkParameters() { return networkParameters; } /** * We return the sig_hash and the wallet_path in every input of * every unsigned transaction. Thus, all the data you need to sign the * input is on the returned JSON. No need to parse the entire thing - * this lets us use multinetwork without BitcoinJ yelling at us. */ public List<String> signaturesFromUnparsedTransaction(JsonObject transactionJson) { List<String> signatures = new ArrayList<>(); for (JsonElement raw : transactionJson.get("inputs").getAsJsonArray()) { JsonObject input = raw.getAsJsonObject(); String sigHash = input.get("sig_hash").getAsString(); String walletPath = input.get("output").getAsJsonObject().get("metadata") .getAsJsonObject().get("wallet_path").getAsString(); signatures.add(base58SignatureForPath(walletPath, new Sha256Hash(sigHash))); } return signatures; } public List<String> signaturesForTransaction(TransactionWrapper transaction) { int inputIndex = 0; List<String> signatures = new ArrayList<String>(); for (InputWrapper inputWrapper : transaction.inputs()) { String walletPath = inputWrapper.walletPath(); Script redeemScript = this.redeemScriptForPath(walletPath); Sha256Hash sigHash = transaction.transaction() .hashForSignature(inputIndex, redeemScript, Transaction.SigHash.ALL, false); String base58Signature = base58SignatureForPath(walletPath, sigHash); signatures.add(base58Signature); inputIndex++; } return signatures; } }