diff --git a/.travis.yml b/.travis.yml index 0d178eaa525..e052378f9c9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ # configuration for https://travis-ci.org/bitcoincash-wallet/bitcoincashj -sudo: false +sudo: sudo dist: trusty language: java -jdk: oraclejdk8 +jdk: openjdk8 addons: postgresql: "9.3" # min supported version cache: @@ -14,6 +14,14 @@ services: install: true # disable default because no need to do the mvn install before mvn verify +before_install: + - wget https://oss.sonatype.org/content/repositories/releases/org/javafxports/dalvik-sdk/8.60.9/dalvik-sdk-8.60.9.zip && unzip dalvik-sdk-8.60.9.zip + - sudo cp dalvik-sdk/rt/lib/ext/jfxrt.jar $JAVA_HOME/jre/lib/ext + - sudo cp dalvik-sdk/rt/lib/jfxswt.jar $JAVA_HOME/jre/lib/ + - sudo cp dalvik-sdk/rt/lib/javafx.properties $JAVA_HOME/jre/lib + - sudo cp dalvik-sdk/rt/lib/javafx.platform.properties $JAVA_HOME/jre/lib + - sudo cp dalvik-sdk/lib/javafx-mx.jar $JAVA_HOME/lib + before_script: - psql -c "create user bitcoinj with password 'password';" -U postgres - psql -c 'create database bitcoinj_test owner bitcoinj;' -U postgres diff --git a/core/pom.xml b/core/pom.xml index 6170a68a162..b016c23145a 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -460,6 +460,26 @@ 1.8 true + + org.apache.commons + commons-lang3 + 3.3 + + + commons-io + commons-io + 2.5 + + + com.madgag.spongycastle + prov + 1.51.0.0 + + + com.google.code.gson + gson + 2.8.2 + @@ -487,4 +507,4 @@ test - + \ No newline at end of file diff --git a/core/src/main/java/org/bitcoinj/core/BlockChain.java b/core/src/main/java/org/bitcoinj/core/BlockChain.java index 6fd6ca2237e..79c6c90f947 100644 --- a/core/src/main/java/org/bitcoinj/core/BlockChain.java +++ b/core/src/main/java/org/bitcoinj/core/BlockChain.java @@ -99,7 +99,7 @@ protected StoredBlock addToBlockStore(StoredBlock storedPrev, Block blockHeader) } @Override - protected void rollbackBlockStore(int height) throws BlockStoreException { + public void rollbackBlockStore(int height) throws BlockStoreException { lock.lock(); try { int currentHeight = getBestChainHeight(); diff --git a/core/src/main/java/org/bitcoinj/core/bip47/BIP47Account.java b/core/src/main/java/org/bitcoinj/core/bip47/BIP47Account.java new file mode 100644 index 00000000000..4f5108bf7fa --- /dev/null +++ b/core/src/main/java/org/bitcoinj/core/bip47/BIP47Account.java @@ -0,0 +1,98 @@ +/* Copyright (c) 2017 Stash + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +package org.bitcoinj.core.bip47; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.crypto.ChildNumber; +import org.bitcoinj.crypto.DeterministicKey; +import org.bitcoinj.crypto.HDKeyDerivation; + +import static org.bitcoinj.core.bip47.BIP47PaymentCode.createMasterPubKeyFromPaymentCode; + +/** + * Created by jimmy on 8/4/17. + */ + +/** + * p>A {@link BIP47Account} is necessary for BIP47 payment channels. It holds the notification key used to derive the + * notification address and the key to derive payment addresses in a channel.

+ * + *

The BIP47 account is at the derivation path

m / 47' / coin_type' / account_id'.
.

+ * + *

Properties:

+ * + */ +public class BIP47Account { + private NetworkParameters mNetworkParameters; + private DeterministicKey mKey; + private int mIndex; + private BIP47PaymentCode mBIP47PaymentCode; + private String mXPub; + + /** + * Constructor expecting a coin_type' derivation path key and the identity number. + */ + public BIP47Account(NetworkParameters parameters, DeterministicKey deterministicKey, int index) { + mNetworkParameters = parameters; + mIndex = index; + mKey = HDKeyDerivation.deriveChildKey(deterministicKey, mIndex | ChildNumber.HARDENED_BIT); + mBIP47PaymentCode = new BIP47PaymentCode(mKey.getPubKey(), mKey.getChainCode()); + mXPub = mKey.serializePubB58(parameters); + } + + /** + * Constructor expecting a Base58Check encoded payment code. + * @throws AddressFormatException if the payment code is invalid + */ + public BIP47Account(NetworkParameters parameters, String strPaymentCode) { + mNetworkParameters = parameters; + mIndex = 0; + mKey = createMasterPubKeyFromPaymentCode(strPaymentCode); + mBIP47PaymentCode = new BIP47PaymentCode(strPaymentCode); + mXPub = mKey.serializePubB58(parameters); + } + + /** Return the Base58Check representation of the payment code*/ + public String getStringPaymentCode() { + return mBIP47PaymentCode.toString(); + } + + public String getXPub() { + return mXPub; + } + + /** Returns the P2PKH address associated with the 0th public key */ + public Address getNotificationAddress() { + return HDKeyDerivation.deriveChildKey(mKey, ChildNumber.ZERO).toAddress(mNetworkParameters); + } + + /** Returns the 0th derivation key */ + public ECKey getNotificationKey() { + return HDKeyDerivation.deriveChildKey(mKey, ChildNumber.ZERO); + } + + /** Return the payment code as is */ + public BIP47PaymentCode getPaymentCode() { + return mBIP47PaymentCode; + } + + /** Returns the nth key. + * @param idx must be between 0 and 2147483647 + */ + public ECKey keyAt(int idx) { + return HDKeyDerivation.deriveChildKey(mKey, new ChildNumber(idx, false)); + } + + public byte[] getPrivKey(int index) { + return HDKeyDerivation.deriveChildKey(mKey, index).getPrivKeyBytes(); + } +} diff --git a/core/src/main/java/org/bitcoinj/core/bip47/BIP47Address.java b/core/src/main/java/org/bitcoinj/core/bip47/BIP47Address.java new file mode 100644 index 00000000000..c572518aa99 --- /dev/null +++ b/core/src/main/java/org/bitcoinj/core/bip47/BIP47Address.java @@ -0,0 +1,50 @@ +/* Copyright (c) 2017 Stash + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.bitcoinj.core.bip47; + +/** + * Created by jimmy on 9/29/17. + */ + +public class BIP47Address { + + private String address; + private int index = 0; + private boolean seen = false; + + public BIP47Address() {} + + public BIP47Address(String address, int index) { + this.address = address; + this.index = index; + } + + public BIP47Address(String address, int index, boolean seen) { + this(address, index); + this.seen = seen; + } + + public String getAddress() { + return address; + } + + public int getIndex() { + return index; + } + + public boolean isSeen() { + return seen; + } + + public void setSeen(boolean seen) { + this.seen = seen; + } + + @Override + public String toString() { + return address; + } +} diff --git a/core/src/main/java/org/bitcoinj/core/bip47/BIP47Channel.java b/core/src/main/java/org/bitcoinj/core/bip47/BIP47Channel.java new file mode 100644 index 00000000000..50247b18c68 --- /dev/null +++ b/core/src/main/java/org/bitcoinj/core/bip47/BIP47Channel.java @@ -0,0 +1,140 @@ +/* Copyright (c) 2017 Stash + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.bitcoinj.core.bip47; + +import org.bitcoinj.core.Sha256Hash; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.kits.BIP47AppKit; +import org.bitcoinj.wallet.bip47.NotSecp256k1Exception; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.spec.InvalidKeySpecException; +import java.util.ArrayList; +import java.util.List; + +import static org.bitcoinj.utils.BIP47Util.getReceiveAddress; + +public class BIP47Channel { + private static final String TAG = "BIP47Channel"; + + private static final int STATUS_NOT_SENT = -1; + private static final int STATUS_SENT_CFM = 1; + + private static final int LOOKAHEAD = 10; + + private String paymentCode; + private String label = ""; + private List incomingAddresses = new ArrayList(); + private List outgoingAddresses = new ArrayList(); + private int status = STATUS_NOT_SENT; + private int currentOutgoingIndex = 0; + private int currentIncomingIndex = -1; + private Sha256Hash ntxHash; + + private static final Logger log = LoggerFactory.getLogger(BIP47Channel.class); + public BIP47Channel() {} + + public BIP47Channel(String paymentCode) { + this.paymentCode = paymentCode; + } + + public BIP47Channel(String paymentCode, String label) { + this(paymentCode); + this.label = label; + } + + public String getPaymentCode() { + return paymentCode; + } + + public void setPaymentCode(String pc) { + paymentCode = pc; + } + + public List getIncomingAddresses() { + return incomingAddresses; + } + + public int getCurrentIncomingIndex() { + return currentIncomingIndex; + } + + public void generateKeys(BIP47AppKit wallet) throws NotSecp256k1Exception, NoSuchProviderException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { + for (int i = 0; i < LOOKAHEAD; i++) { + ECKey key = getReceiveAddress(wallet, paymentCode, i).getReceiveECKey(); + Address address = wallet.getAddressOfKey(key); + + log.debug("New address generated"); + log.debug(address.toString()); + wallet.importKey(key); + incomingAddresses.add(i, new BIP47Address(address.toString(), i)); + } + + currentIncomingIndex = LOOKAHEAD - 1; + } + + public BIP47Address getIncomingAddress(String address) { + for (BIP47Address bip47Address: incomingAddresses) { + if (bip47Address.getAddress().equals(address)) { + return bip47Address; + } + } + return null; + } + + public void addNewIncomingAddress(String newAddress, int nextIndex) { + incomingAddresses.add(nextIndex, new BIP47Address(newAddress, nextIndex)); + currentIncomingIndex = nextIndex; + } + + public String getLabel() { + return label; + } + + public void setLabel(String l) { + label = l; + } + + public List getOutgoingAddresses() { + return outgoingAddresses; + } + + public boolean isNotificationTransactionSent() { + return status == STATUS_SENT_CFM; + } + + public void setStatusSent() { + status = STATUS_SENT_CFM; + } + + public int getCurrentOutgoingIndex() { + return currentOutgoingIndex; + } + + public void incrementOutgoingIndex() { + currentOutgoingIndex++; + } + + public void addAddressToOutgoingAddresses(String address) { + outgoingAddresses.add(address); + } + + public void setStatusNotSent() { + status = STATUS_NOT_SENT; + } + + public Sha256Hash getNtxHash() { return ntxHash; } + + public void setNtxHash(Sha256Hash ntxHash) { + this.ntxHash = ntxHash; + } +} diff --git a/core/src/main/java/org/bitcoinj/core/bip47/BIP47PaymentAddress.java b/core/src/main/java/org/bitcoinj/core/bip47/BIP47PaymentAddress.java new file mode 100644 index 00000000000..eb3fbcb8007 --- /dev/null +++ b/core/src/main/java/org/bitcoinj/core/bip47/BIP47PaymentAddress.java @@ -0,0 +1,144 @@ +/* Copyright (c) 2017 Stash + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.bitcoinj.core.bip47; + +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.spec.InvalidKeySpecException; +import org.bitcoinj.core.AddressFormatException; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.crypto.BIP47SecretPoint; +import org.bitcoinj.wallet.bip47.NotSecp256k1Exception; +import org.spongycastle.asn1.x9.X9ECParameters; +import org.spongycastle.crypto.ec.CustomNamedCurves; +import org.spongycastle.crypto.params.ECDomainParameters; +import org.spongycastle.math.ec.ECPoint; + +/** + * p>A {@link BIP47PaymentAddress} is derived for account deposits in a bip47 channel. It is used by a recipient's bip47 wallet to derive and watch deposits. It + * is also used by a sender's bip47 wallet to calculate the next addresses to send a deposit to.

+ * + * The BIP47 BIP47PaymentAddress is at the derivation path
m / 47' / coin_type' / account_id' / idx' .
.

+ * + *

Properties:

+ *
    + *
  • The account_id is irrelevant in this class, it's implied in privKey.
  • + *
  • The owner of BIP47PaymentCode is not the same owner of privKey.
  • + *
+ */ +public class BIP47PaymentAddress { + // if we are receiving, this is the sender's payment code + // if we are sending, this is the receiver's payment code + private org.bitcoinj.core.bip47.BIP47PaymentCode BIP47PaymentCode = null; + // the index to use in the derivation path + private int index = 0; + // the corresponding hardedened key bytes at the derivation path + private byte[] privKey = null; + // the network used for formatting + private NetworkParameters networkParameters; + // give the values for "a", "b", "G" in the ECDSA curve used in Bitcoin (https://en.bitcoin.it/wiki/Secp256k1) + private static final X9ECParameters CURVE_PARAMS = CustomNamedCurves.getByName("secp256k1"); + // create the curve + private static final ECDomainParameters CURVE; + static { + CURVE = new ECDomainParameters(CURVE_PARAMS.getCurve(), CURVE_PARAMS.getG(), CURVE_PARAMS.getN(), CURVE_PARAMS.getH()); + } + + /** Creates a BIP47PaymentAddress object that the sender will use to pay, using the hardened key at idx */ + public BIP47PaymentAddress(NetworkParameters networkParameters, BIP47PaymentCode BIP47PaymentCode, int index, byte[] privKey) throws AddressFormatException { + this.BIP47PaymentCode = BIP47PaymentCode; + this.index = index; + this.privKey = privKey; + this.networkParameters = networkParameters; + } + + /** Creates a HD key to send a deposit */ + public ECKey getSendECKey() throws AddressFormatException, InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, IllegalStateException, InvalidKeySpecException, NotSecp256k1Exception { + return this.getSendECKey(this.secretPoint()); + } + + /** Derives a deposit address to watch to receive payments from BIP47PaymentCode's owner*/ + public ECKey getReceiveECKey() throws AddressFormatException, InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, IllegalStateException, InvalidKeySpecException, NotSecp256k1Exception { + return this.getReceiveECKey(this.secretPoint()); + } + + /* Use the generator "G" by */ + //public ECPoint get_sG() throws AddressFormatException, InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, IllegalStateException, InvalidKeySpecException, NotSecp256k1Exception { + // return CURVE_PARAMS.getG().multiply(this.getSecretPoint()); + //} + + /* Accesor for the secret point between sender and receiver */ + public BIP47SecretPoint getSharedSecret() throws AddressFormatException, InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, IllegalStateException, InvalidKeySpecException { + return this.sharedSecret(); + } + + //public BigInteger getSecretPoint() throws AddressFormatException, InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, IllegalStateException, InvalidKeySpecException, NotSecp256k1Exception { + // return this.secretPoint(); + //} + + public ECPoint getECPoint() throws AddressFormatException, InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, IllegalStateException, InvalidKeySpecException { + ECKey ecKey = ECKey.fromPublicOnly(this.BIP47PaymentCode.derivePubKeyAt(this.networkParameters, this.index)); + return ecKey.getPubKeyPoint(); + } + + /** Returns the scalar shared secret */ + public byte[] hashSharedSecret() throws AddressFormatException, InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, IllegalStateException, InvalidKeySpecException { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(this.getSharedSecret().ECDHSecretAsBytes()); + return hash; + } + + /* Multply a times the generator G */ + private ECPoint get_sG(BigInteger s) { + return CURVE_PARAMS.getG().multiply(s); + } + + /* Derives the key for the payment address where the BIP47PaymentCode's owner will be watching for deposits */ + private ECKey getSendECKey(BigInteger s) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException { + ECPoint ecPoint = this.getECPoint(); + ECPoint sG = this.get_sG(s); + ECKey ecKey = ECKey.fromPublicOnly(ecPoint.add(sG).getEncoded(true)); + return ecKey; + } + + /* Calculates the ephemeral hardened key used to generate the P2PKH address where a deposit will be received */ + private ECKey getReceiveECKey(BigInteger s) { + BigInteger privKeyValue = ECKey.fromPrivate(this.privKey).getPrivKey(); + ECKey ecKey = ECKey.fromPrivate(this.addSecp256k1(privKeyValue, s)); + return ecKey; + } + + /* Adds two keys together */ + private BigInteger addSecp256k1(BigInteger b1, BigInteger b2) { + BigInteger ret = b1.add(b2); + return ret.bitLength() > CURVE.getN().bitLength()?ret.mod(CURVE.getN()):ret; + } + + /* Return the ECDH shared secret between us and the owner of BIP47PaymentCode */ + private BIP47SecretPoint sharedSecret() throws AddressFormatException, InvalidKeySpecException, InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, NoSuchProviderException { + byte[] pubKey = this.BIP47PaymentCode.derivePubKeyAt(this.networkParameters, this.index); + return new BIP47SecretPoint(this.privKey, pubKey); + } + + /* Returns true if the given point "b" is in the curve */ + private boolean isSecp256k1(BigInteger b) { + return b.compareTo(BigInteger.ONE) > 0 && b.bitLength() <= CURVE.getN().bitLength(); + } + + /** Returns a SHA256 mask of the secret point */ + private BigInteger secretPoint() throws AddressFormatException, InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException, NotSecp256k1Exception { + BigInteger s = new BigInteger(1, this.hashSharedSecret()); + if(!this.isSecp256k1(s)) { + throw new NotSecp256k1Exception("secret point not on Secp256k1 curve"); + } else { + return s; + } + } +} diff --git a/core/src/main/java/org/bitcoinj/core/bip47/BIP47PaymentCode.java b/core/src/main/java/org/bitcoinj/core/bip47/BIP47PaymentCode.java new file mode 100644 index 00000000000..b82f671da5f --- /dev/null +++ b/core/src/main/java/org/bitcoinj/core/bip47/BIP47PaymentCode.java @@ -0,0 +1,257 @@ +/* Copyright (c) 2017 Stash + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.bitcoinj.core.bip47; + +import java.math.BigInteger; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.bitcoinj.core.*; +import org.bitcoinj.crypto.ChildNumber; +import org.bitcoinj.crypto.DeterministicKey; +import org.bitcoinj.crypto.HDKeyDerivation; + +public class BIP47PaymentCode { + private static final int PUBLIC_KEY_Y_OFFSET = 2; + private static final int PUBLIC_KEY_X_OFFSET = 3; + private static final int CHAIN_OFFSET = 35; + private static final int PUBLIC_KEY_X_LEN = 32; + private static final int PUBLIC_KEY_Y_LEN = 1; + private static final int CHAIN_LEN = 32; + private static final int PAYLOAD_LEN = 80; + private String strPaymentCode = null; + private byte[] pubkey = null; + private byte[] chain = null; + + public BIP47PaymentCode() { + this.strPaymentCode = null; + this.pubkey = null; + this.chain = null; + } + + public BIP47PaymentCode(String payment_code) throws AddressFormatException { + this.strPaymentCode = payment_code; + this.pubkey = this.parse().getLeft(); + this.chain = this.parse().getRight(); + } + + public BIP47PaymentCode(byte[] payload) { + if(payload.length == 80) { + this.pubkey = new byte[33]; + this.chain = new byte[32]; + System.arraycopy(payload, 2, this.pubkey, 0, 33); + System.arraycopy(payload, 35, this.chain, 0, 32); + this.strPaymentCode = this.makeV1(); + } + } + + public BIP47PaymentCode(byte[] pubkey, byte[] chain) { + this.pubkey = pubkey; + this.chain = chain; + this.strPaymentCode = this.makeV1(); + } + + public byte[] getPayload() throws AddressFormatException { + byte[] pcBytes = Base58.decodeChecked(this.strPaymentCode); + byte[] payload = new byte[80]; + System.arraycopy(pcBytes, 1, payload, 0, payload.length); + return payload; + } + + public int getType() throws AddressFormatException { + byte[] payload = this.getPayload(); + ByteBuffer bb = ByteBuffer.wrap(payload); + byte type = bb.get(); + return type; + } + + public byte[] decode() throws AddressFormatException { + return Base58.decode(this.strPaymentCode); + } + + public byte[] decodeChecked() throws AddressFormatException { + return Base58.decodeChecked(this.strPaymentCode); + } + + public byte[] getPubKey() { + return this.pubkey; + } + + public byte[] getChain() { + return this.chain; + } + + public String toString() { + return this.strPaymentCode; + } + + public static byte[] getMask(byte[] sPoint, byte[] oPoint) { + Mac sha512_HMAC = null; + byte[] mac_data = null; + + try { + sha512_HMAC = Mac.getInstance("HmacSHA512"); + SecretKeySpec secretkey = new SecretKeySpec(oPoint, "HmacSHA512"); + sha512_HMAC.init(secretkey); + mac_data = sha512_HMAC.doFinal(sPoint); + } catch (InvalidKeyException var5) { + } catch (NoSuchAlgorithmException var6) { + } + + return mac_data; + } + + public static byte[] blind(byte[] payload, byte[] mask) throws AddressFormatException { + byte[] ret = new byte[80]; + byte[] pubkey = new byte[32]; + byte[] chain = new byte[32]; + byte[] buf0 = new byte[32]; + byte[] buf1 = new byte[32]; + System.arraycopy(payload, 0, ret, 0, 80); + System.arraycopy(payload, 3, pubkey, 0, 32); + System.arraycopy(payload, 35, chain, 0, 32); + System.arraycopy(mask, 0, buf0, 0, 32); + System.arraycopy(mask, 32, buf1, 0, 32); + System.arraycopy(xor(pubkey, buf0), 0, ret, 3, 32); + System.arraycopy(xor(chain, buf1), 0, ret, 35, 32); + return ret; + } + + private Pair parse() throws AddressFormatException { + byte[] pcBytes = Base58.decodeChecked(this.strPaymentCode); + ByteBuffer bb = ByteBuffer.wrap(pcBytes); + if(bb.get() != 71) { + throw new AddressFormatException("invalid payment code version"); + } else { + byte[] chain = new byte[32]; + byte[] pub = new byte[33]; + bb.get(); + bb.get(); + bb.get(pub); + if(pub[0] != 2 && pub[0] != 3) { + throw new AddressFormatException("invalid public key"); + } else { + bb.get(chain); + return Pair.of(pub, chain); + } + } + } + + private String makeV1() { + return this.make(1); + } + + private String makeV2() { + return this.make(2); + } + + private String make(int type) { + String ret = null; + byte[] payload = new byte[80]; + byte[] payment_code = new byte[81]; + + for(int checksum = 0; checksum < payload.length; ++checksum) { + payload[checksum] = 0; + } + + payload[0] = (byte)type; + payload[1] = 0; + System.arraycopy(this.pubkey, 0, payload, 2, this.pubkey.length); + System.arraycopy(this.chain, 0, payload, 35, this.chain.length); + payment_code[0] = 71; + System.arraycopy(payload, 0, payment_code, 1, payload.length); + byte[] var7 = Arrays.copyOfRange(Sha256Hash.hashTwice(payment_code), 0, 4); + byte[] payment_code_checksum = new byte[payment_code.length + var7.length]; + System.arraycopy(payment_code, 0, payment_code_checksum, 0, payment_code.length); + System.arraycopy(var7, 0, payment_code_checksum, payment_code_checksum.length - 4, var7.length); + ret = Base58.encode(payment_code_checksum); + return ret; + } + + private DeterministicKey createMasterPubKeyFromBytes(byte[] pub, byte[] chain) throws AddressFormatException { + return HDKeyDerivation.createMasterPubKeyFromBytes(pub, chain); + } + + private static byte[] xor(byte[] a, byte[] b) { + if(a.length != b.length) { + return null; + } else { + byte[] ret = new byte[a.length]; + + for(int i = 0; i < a.length; ++i) { + ret[i] = (byte)(b[i] ^ a[i]); + } + + return ret; + } + } + + public boolean isValid() { + try { + byte[] afe = Base58.decodeChecked(this.strPaymentCode); + ByteBuffer byteBuffer = ByteBuffer.wrap(afe); + if(byteBuffer.get() != 71) { + throw new AddressFormatException("invalid version: " + this.strPaymentCode); + } else { + byte[] chain = new byte[32]; + byte[] pub = new byte[33]; + byteBuffer.get(); + byteBuffer.get(); + byteBuffer.get(pub); + byteBuffer.get(chain); + ByteBuffer pubBytes = ByteBuffer.wrap(pub); + byte firstByte = pubBytes.get(); + return firstByte == 2 || firstByte == 3; + } + } catch (BufferUnderflowException var7) { + return false; + } catch (AddressFormatException var8) { + return false; + } + } + + public static DeterministicKey createMasterPubKeyFromPaymentCode(String payment_code_str) throws AddressFormatException { + byte[] paymentCodeBytes = Base58.decodeChecked(payment_code_str); + ByteBuffer bb = ByteBuffer.wrap(paymentCodeBytes); + if(bb.get() != 71) { + throw new AddressFormatException("invalid payment code version"); + } else { + byte[] chain = new byte[32]; + byte[] pub = new byte[33]; + bb.get(); + bb.get(); + bb.get(pub); + bb.get(chain); + return HDKeyDerivation.createMasterPubKeyFromBytes(pub, chain); + } + } + + /** Returns the pubkey on the ith derivation path */ + public byte[] derivePubKeyAt(NetworkParameters networkParameters, int i) throws AddressFormatException { + DeterministicKey key = createMasterPubKeyFromPaymentCode(this.strPaymentCode); + DeterministicKey dk = HDKeyDerivation.deriveChildKey(key, new ChildNumber(i, false)); + + ECKey ecKey; + if(dk.hasPrivKey()) { + byte[] now = ArrayUtils.addAll(new byte[1], dk.getPrivKeyBytes()); + ecKey = ECKey.fromPrivate(new BigInteger(now), true); + } else { + ecKey = ECKey.fromPublicOnly(dk.getPubKey()); + } + + long now1 = Utils.now().getTime() / 1000L; + ecKey.setCreationTimeSeconds(now1); + + return ecKey.getPubKey(); + } +} diff --git a/core/src/main/java/org/bitcoinj/crypto/BIP47SecretPoint.java b/core/src/main/java/org/bitcoinj/crypto/BIP47SecretPoint.java new file mode 100644 index 00000000000..9f7be0aac2f --- /dev/null +++ b/core/src/main/java/org/bitcoinj/crypto/BIP47SecretPoint.java @@ -0,0 +1,92 @@ +/* Copyright (c) 2017 Stash + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.bitcoinj.crypto; + +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Security; +import java.security.spec.InvalidKeySpecException; +import javax.crypto.KeyAgreement; +import javax.crypto.SecretKey; + +import org.spongycastle.jce.ECNamedCurveTable; +import org.spongycastle.jce.provider.BouncyCastleProvider; +import org.spongycastle.jce.spec.ECParameterSpec; +import org.spongycastle.jce.spec.ECPrivateKeySpec; +import org.spongycastle.jce.spec.ECPublicKeySpec; +import org.spongycastle.util.encoders.Hex; + +public class BIP47SecretPoint { + private static final ECParameterSpec params = ECNamedCurveTable.getParameterSpec("secp256k1"); + + static { + Security.addProvider(new BouncyCastleProvider()); + } + + private PrivateKey privKey = null; + private PublicKey pubKey = null; + private KeyFactory kf = null; + + public BIP47SecretPoint() { + } + + public BIP47SecretPoint(byte[] dataPrv, byte[] dataPub) throws InvalidKeySpecException, InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, NoSuchProviderException { + this.kf = KeyFactory.getInstance("ECDH", "SC"); + this.privKey = this.loadPrivateKey(dataPrv); + this.pubKey = this.loadPublicKey(dataPub); + } + + public PrivateKey getPrivKey() { + return this.privKey; + } + + public void setPrivKey(PrivateKey privKey) { + this.privKey = privKey; + } + + public PublicKey getPubKey() { + return this.pubKey; + } + + public void setPubKey(PublicKey pubKey) { + this.pubKey = pubKey; + } + + public byte[] ECDHSecretAsBytes() throws InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, NoSuchProviderException { + return this.ECDHSecret().getEncoded(); + } + + public boolean isShared(BIP47SecretPoint secret) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException { + return this.equals(secret); + } + + private SecretKey ECDHSecret() throws InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, NoSuchProviderException { + KeyAgreement ka = KeyAgreement.getInstance("ECDH", "SC"); + ka.init(this.privKey); + ka.doPhase(this.pubKey, true); + SecretKey secret = ka.generateSecret("AES"); + return secret; + } + + private boolean equals(BIP47SecretPoint secret) throws InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, NoSuchProviderException { + return Hex.toHexString(this.ECDHSecretAsBytes()).equals(Hex.toHexString(secret.ECDHSecretAsBytes())); + } + + private PublicKey loadPublicKey(byte[] data) throws InvalidKeySpecException { + ECPublicKeySpec pubKey = new ECPublicKeySpec(params.getCurve().decodePoint(data), params); + return this.kf.generatePublic(pubKey); + } + + private PrivateKey loadPrivateKey(byte[] data) throws InvalidKeySpecException { + ECPrivateKeySpec prvkey = new ECPrivateKeySpec(new BigInteger(1, data), params); + return this.kf.generatePrivate(prvkey); + } +} diff --git a/core/src/main/java/org/bitcoinj/kits/BIP47AppKit.java b/core/src/main/java/org/bitcoinj/kits/BIP47AppKit.java new file mode 100644 index 00000000000..55f4e49061f --- /dev/null +++ b/core/src/main/java/org/bitcoinj/kits/BIP47AppKit.java @@ -0,0 +1,990 @@ +/* Copyright (c) 2017 Stash + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.bitcoinj.kits; + + +import com.google.common.net.InetAddresses; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; +import org.bitcoinj.core.*; +import org.bitcoinj.core.bip47.*; +import org.bitcoinj.crypto.BIP47SecretPoint; +import org.bitcoinj.wallet.*; +import org.bitcoinj.utils.BIP47Util; +import org.bitcoinj.wallet.bip47.NotSecp256k1Exception; +import org.bitcoinj.wallet.bip47.listeners.BlockchainDownloadProgressTracker; +import org.bitcoinj.wallet.bip47.listeners.TransactionEventListener; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.bitcoinj.crypto.ChildNumber; +import org.bitcoinj.crypto.DeterministicKey; +import org.bitcoinj.crypto.HDKeyDerivation; +import org.bitcoinj.net.discovery.DnsDiscovery; +import org.bitcoinj.script.Script; +import org.bitcoinj.script.ScriptBuilder; +import org.bitcoinj.store.BlockStore; +import org.bitcoinj.store.BlockStoreException; +import org.bitcoinj.store.SPVBlockStore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.lang.reflect.Type; +import java.nio.charset.Charset; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.spec.InvalidKeySpecException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +import static org.bitcoinj.core.Utils.join; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.bitcoinj.utils.BIP47Util.getReceiveAddress; +import static org.bitcoinj.utils.BIP47Util.getSendAddress; + +/** + * Created by jimmy on 9/28/17. + */ + +/** + *

Runs a spv wallet and supports BIP 47 payments for coins. You will + * need to instantiate one wallet per supported coin.

+ * + *

It produces two files in a designated directory. The directory name is the coin name. and is created in workingDirectory:

+ *
    + * The .spvchain (blockstore): maintains a maximum # of headers mapped to memory (5000) + * The .wallet: stores the wallet with txs, can be encrypted, storing keys + *
+ * + *

By using this kit, your wallet will import keys for bip 47 payment addresses, when a BIP 47 + * notification transaction is received.

+ * + */ +public class BIP47AppKit { + private static final String TAG = "BIP47AppKit"; + + // the coin name that this wallet supports. Can be: BTC, tBTC, BCH, tBCH + private String coinName; + // fee parameters and network metadata + private NetworkParameters params; + // the blokstore is used by a blockchain as a memory data structure + private volatile BlockChain vChain; + private volatile BlockStore vStore; + private volatile org.bitcoinj.wallet.Wallet vWallet; + // sync with the blockchain by using a peergroup + private volatile PeerGroup vPeerGroup; + + // the directory will have the spvchain and the wallet files + private final File directory; + private volatile File vWalletFile; + // Wether this wallet is restored from a BIP39 seed and will need to replay the complete blockchain + // Will be null if it's not a restored wallet. + private DeterministicSeed restoreFromSeed; + + // Support for BIP47-type accounts. Only one account is currently handled in this wallet. + private List mAccounts = new ArrayList(1); + + // The progress tracker will callback the listener with a porcetage of the blockchain that it has downloaded, while downloading.. + private BlockchainDownloadProgressTracker mBlockchainDownloadProgressTracker; + + // This wallet allows one listener to be invoked when there are coins received and + private TransactionEventListener mCoinsReceivedEventListener = null; + // one listener when the transaction confidence changes + private TransactionEventListener mTransactionConfidenceListener = null; + + private boolean mBlockchainDownloadStarted = false; + + // The payment channels indexed by payment codes. + // A payment channel is created and saved if: + // - someone sends a notification transaction to this wallet's notifiction address + // - this wallet creates a notification transaction to a payment channel. + // + // It doesn't check if the notification transactions are mined before adding a payment code. + // If you want to know a transaction's confidence, see #{@link Transaction.getConfidence()} + private ConcurrentHashMap bip47MetaData = new ConcurrentHashMap(); + private static final Logger log = LoggerFactory.getLogger(BIP47AppKit.class); + + /** + *

Creates a new wallet for a coinName, the .spvchain and .wallet files in workingDir/coinName.

+ * Any keys will be derived from deterministicSeed. + */ + public BIP47AppKit(String coinName, NetworkParameters params, File walletDirectory, @Nullable DeterministicSeed deterministicSeed) throws Exception { + this.coinName = checkNotNull(coinName); + this.params = checkNotNull(params); + this.directory = new File(walletDirectory, coinName); + this.restoreFromSeed = deterministicSeed; + + // ensure directory exists + if (!directory.exists()) { + if (!directory.mkdirs()) { + throw new IOException("Could not create directory " + directory.getAbsolutePath()); + } + } + + File chainFile = new File(directory, coinName + ".spvchain"); + boolean chainFileExists = chainFile.exists(); + // point to the file with the (possibly existent) BIP47AppKit + vWalletFile = new File(directory, coinName + ".wallet"); + log.debug("BIP47AppKit: "+coinName); + + // replay the wallet if deterministicSeed is defined or if it's chain file is deleted (as a trigger to replay it) + boolean shouldReplayWallet = (vWalletFile.exists() && !chainFileExists) || restoreFromSeed != null; + + Context.propagate(new Context(params)); + vWallet = createOrLoadWallet(shouldReplayWallet); + setAccount(); + + Address notificationAddress = mAccounts.get(0).getNotificationAddress(); + log.debug("BIP47AppKit notification address: "+notificationAddress.toString()); + + if (!vWallet.isAddressWatched(notificationAddress)) { + vWallet.addWatchedAddress(notificationAddress); + } + + vWallet.allowSpendingUnconfirmedTransactions(); + + log.debug(vWallet.toString()); + + // Initiate Bitcoin network objects (block store, blockchain and peer group) + + // open the blockstore file + vStore = new SPVBlockStore(params, chainFile); + + // create a fresh blockstore file before restoring a wallet + if (restoreFromSeed != null && chainFileExists) { + log.info( "Deleting the chain file in preparation from restore."); + vStore.close(); + if (!chainFile.delete()) + log.warn("start: ", new IOException("Failed to delete chain file in preparation for restore.")); + vStore = new SPVBlockStore(params, chainFile); + } + + try { + // create the blockchain object using the file-backed blockstore + vChain = new BlockChain(params, vStore); + } catch (BlockStoreException e){ + + // - we can create a new blockstore in case it is corrupted, the wallet should have a last height + if (chainFile.exists()) { + log.debug("deleteSpvFile: exits"); + chainFile.delete(); + } + + vStore = new SPVBlockStore(params, chainFile); + vChain = new BlockChain(params, vStore); + } + + // add the wallet so that syncing and rolling the chain can affect this wallet + vChain.addWallet(vWallet); + derivePeerGroup(); + addTransactionsListener(); + } + + // create peergroup for the blockchain + private void derivePeerGroup(){ + Context.propagate(new Context(params)); + if (vPeerGroup == null) + vPeerGroup = new PeerGroup(params, vChain); + + // add Stash-Crypto dedicated nodes for BCH and tBCH + if (coinName.equals("BCH")) { + vPeerGroup.setMaxConnections(vPeerGroup.DEFAULT_CONNECTIONS); + vPeerGroup.addAddress(new PeerAddress(InetAddresses.forString("158.69.119.35"), 8333)); + vPeerGroup.addAddress(new PeerAddress(InetAddresses.forString("144.217.73.86"), 8333)); + // bitcoin abc from shodan.io + //vPeerGroup.addAddress(new PeerAddress(InetAddresses.forString("106.14.105.56"), 8333)); + //vPeerGroup.addAddress(new PeerAddress(InetAddresses.forString("52.211.14.233"), 8333)); + //vPeerGroup.addAddress(new PeerAddress(InetAddresses.forString("50.39.245.26"), 8333)); + //vPeerGroup.addAddress(new PeerAddress(InetAddresses.forString("52.57.14.67"), 8333)); + // bucash + //vPeerGroup.addAddress(new PeerAddress(InetAddresses.forString("5.44.97.110"), 8333)); + //vPeerGroup.addAddress(new PeerAddress(InetAddresses.forString("185.69.52.180"), 8333)); + } else if (coinName.equals("tBCH")) { + vPeerGroup.setMaxConnections(vPeerGroup.DEFAULT_CONNECTIONS); + // stash crypto + vPeerGroup.addAddress(new PeerAddress(InetAddresses.forString("158.69.119.35"), 18333)); + vPeerGroup.addAddress(new PeerAddress(InetAddresses.forString("144.217.73.86"), 18333)); + // bitcoin abc from shodan.io + //vPeerGroup.addAddress(new PeerAddress(InetAddresses.forString("61.100.182.189"), 18333)); + //vPeerGroup.addAddress(new PeerAddress(InetAddresses.forString("47.74.186.127"), 18333)); + } + + // connect to peer running in localhost (127.0.0.1) + vPeerGroup.setUseLocalhostPeerWhenPossible(true); + // connect to peers in the blockchain network + vPeerGroup.addPeerDiscovery(new DnsDiscovery(params)); + + // add the wallet to the peers so that every peer listener can find this wallet e.g. to invoke listeners + vPeerGroup.addWallet(vWallet); + } + + // BIP47-specific listener + // When a new *notification* transaction is received: + // - new keys are generated and imported for incoming payments in the bip47 account/contact payment channel + // - the chain is rolled back 2 blocks so that payment transactions are not missed if in the same block as the notification transaction. + // + // When a new *payment* transaction is received: + // - a new key is generated and imported to the wallet + private void addTransactionsListener(){ + this.addOnReceiveTransactionListener(new TransactionEventListener() { + @Override + public void onTransactionReceived(BIP47AppKit bip47AppKit, Transaction transaction) { + + if (isNotificationTransaction(transaction)) { + log.debug("Valid notification transaction received"); + BIP47PaymentCode BIP47PaymentCode = getPaymentCodeInNotificationTransaction(transaction); + if (BIP47PaymentCode == null) { + log.warn("Error decoding payment code in tx {}", transaction); + } else { + log.debug("Payment Code: " + BIP47PaymentCode); + boolean needsSaving = savePaymentCode(BIP47PaymentCode); + if (needsSaving) { + saveBip47MetaData(); + } + } + } else if (isToBIP47Address(transaction)) { + log.debug("New BIP47 payment received to address: "+getAddressOfReceived(transaction)); + boolean needsSaving = generateNewBip47IncomingAddress(getAddressOfReceived(transaction).toString()); + if (needsSaving) { + saveBip47MetaData(); + } + String paymentCode = getPaymentCodeForAddress(getAddressOfReceived(transaction).toString()); + log.debug("Received tx for Payment Code: " + paymentCode); + } else { + Coin valueSentToMe = getValueSentToMe(transaction); + log.debug("Received tx for "+valueSentToMe.toFriendlyString() + ":" + transaction); + } + } + + @Override + public void onTransactionConfidenceEvent(BIP47AppKit bip47AppKit, Transaction transaction) { + return; + } + }); + } + + // if coinName/coinName.wallet exists, we load it as a core BIP47AppKit and then manually set each of the bip47 properties + private org.bitcoinj.wallet.Wallet createOrLoadWallet(boolean shouldReplayWallet) throws Exception { + org.bitcoinj.wallet.Wallet wallet; + + if (vWalletFile.exists()) { + wallet = loadWallet(shouldReplayWallet); + } else { + // create an empty wallet + wallet = createWallet(); + // with a seed + wallet.freshReceiveKey(); + // reload the wallet + wallet.saveToFile(vWalletFile); + wallet = loadWallet(false); + } + + // every 5 seconds let's persist the transactions, keys, last block height, watched scripts, etc. + // does not persist channels recurrently, instead payment channels are currently saved in a separete file (.bip47 extension). + wallet.autosaveToFile(vWalletFile, 5, TimeUnit.SECONDS, null); + + return wallet; + } + + // Load an offline wallet from a file and return a @{link org.bitcoinj.wallet.BIP47AppKit}. + // If shouldReplayWallet is false, the wallet last block is reset to -1 + private org.bitcoinj.wallet.Wallet loadWallet(boolean shouldReplayWallet) throws Exception { + FileInputStream walletStream = new FileInputStream(vWalletFile); + Protos.Wallet proto = WalletProtobufSerializer.parseToProto(walletStream); + final WalletProtobufSerializer serializer = new WalletProtobufSerializer(); + org.bitcoinj.wallet.Wallet wallet = serializer.readWallet(params, null, proto); + if (shouldReplayWallet) + wallet.reset(); + return wallet; + } + + private org.bitcoinj.wallet.Wallet createWallet() { + KeyChainGroup kcg; + if (restoreFromSeed != null) + kcg = new KeyChainGroup(params, restoreFromSeed); + else + kcg = new KeyChainGroup(params); + return new org.bitcoinj.wallet.Wallet(params, kcg); // default + } + + public String getCoinName() { + return coinName; + } + + /** + *

Create the account M/47'/0'/0' from the seed as a Bip47Account.

+ * + *

After deriving, this wallet's payment code is available in @{link Bip47Wallet.getPaymentCode()}

+ */ + public void setAccount() { + byte[] hd_seed = this.restoreFromSeed != null ? + this.restoreFromSeed.getSeedBytes() : + vWallet.getKeyChainSeed().getSeedBytes(); + + DeterministicKey mKey = HDKeyDerivation.createMasterPrivateKey(hd_seed); + DeterministicKey purposeKey = HDKeyDerivation.deriveChildKey(mKey, 47 | ChildNumber.HARDENED_BIT); + DeterministicKey coinKey = HDKeyDerivation.deriveChildKey(purposeKey, ChildNumber.HARDENED_BIT); + + BIP47Account account = new BIP47Account(params, coinKey, 0); + + mAccounts.clear(); + mAccounts.add(account); + } + + /* + *

Connect this wallet to the network. Watch notification and payment transactions.

+ */ + public void startBlockchainDownload() { + if (!isStarted() && !mBlockchainDownloadStarted) { + log.debug("Starting blockchain download."); + vPeerGroup.start(); + vPeerGroup.startBlockChainDownload(mBlockchainDownloadProgressTracker); + mBlockchainDownloadStarted = true; + } else { + log.warn("Attempted to start blockchain download but it is already started."); + } + } + + public List getConnectedPeers() { + return vPeerGroup.getConnectedPeers(); + } + + /** + * Disconnects the wallet from the network + */ + public void stop() { + if (!isStarted()) { + return; + } + + log.debug("Stopping peergroup"); + vPeerGroup.stop(); + try { + log.debug("Saving wallet"); + vWallet.saveToFile(vWalletFile); + } catch (IOException e) { + e.printStackTrace(); + } + + log.debug("stopWallet: closing store"); + try { + if (vStore != null) + vStore.close(); + } catch (Exception e) { + e.printStackTrace(); + } + + vStore = null; + vPeerGroup = null; + mBlockchainDownloadStarted = false; + derivePeerGroup(); + + mBlockchainDownloadStarted = false; + + log.debug("stopWallet: Done"); + } + + public boolean isStarted() { + if (vPeerGroup == null) + return false; + return vPeerGroup.isRunning(); + } + + public void setBlockchainDownloadProgressTracker(BlockchainDownloadProgressTracker downloadProgressTracker) { + mBlockchainDownloadProgressTracker = downloadProgressTracker; + } + + /** + *

Reads the channels from .bip47 file. Return true if any payment code was loaded.

+ */ + public boolean loadBip47MetaData() { + String jsonString = readBip47MetaDataFile(); + + if (StringUtils.isEmpty(jsonString)) { + return false; + } + + log.debug("loadBip47MetaData: "+jsonString); + + return importBip47MetaData(jsonString); + } + + /** + *

Reads the channels from .bip47 file. Return true if any payment code was loaded.

+ */ + public String readBip47MetaDataFile() { + File file = new File(directory, coinName.concat(".bip47")); + String jsonString; + try { + jsonString = FileUtils.readFileToString(file, Charset.defaultCharset()); + } catch (IOException e){ + log.debug("Creating BIP47 wallet file at " + file.getAbsolutePath() + " ..."); + saveBip47MetaData(); + loadBip47MetaData(); + return null; + } + + return jsonString; + } + + /** + *

Load channels from json. Return true if any payment code was loaded.

+ */ + public boolean importBip47MetaData(String jsonString) { + log.debug("loadBip47MetaData: "+jsonString); + + Gson gson = new Gson(); + Type collectionType = new TypeToken>(){}.getType(); + try { + List BIP47ChannelList = gson.fromJson(jsonString, collectionType); + if (BIP47ChannelList != null) { + for (BIP47Channel BIP47Channel : BIP47ChannelList) { + bip47MetaData.put(BIP47Channel.getPaymentCode(), BIP47Channel); + } + } + } catch (JsonSyntaxException e) { + return true; + } + return false; + } + + /** + *

Persists the .bip47 file with the channels.

+ */ + public synchronized void saveBip47MetaData() { + try { + vWallet.saveToFile(vWalletFile); + } catch (IOException io){ + log.error("Failed to save wallet file",io); + } + + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + String json = gson.toJson(bip47MetaData.values()); + + log.debug("saveBip47MetaData: "+json); + + File file = new File(directory, coinName.concat(".bip47")); + + try { + FileUtils.writeStringToFile(file, json, Charset.defaultCharset(), false); + log.debug("saveBip47MetaData: saved"); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /**

A listener is added to be invoked when the wallet sees an incoming transaction.

*/ + public void addOnReceiveTransactionListener(TransactionEventListener transactionEventListener){ + if (this.mCoinsReceivedEventListener != null) + vWallet.removeCoinsReceivedEventListener(mCoinsReceivedEventListener); + + transactionEventListener.setWallet(this); + vWallet.addCoinsReceivedEventListener(transactionEventListener); + + mCoinsReceivedEventListener = transactionEventListener; + } + + /**

A listener is added to be invoked when the wallet receives blocks and builds confidence on a transaction

*/ + public void addTransactionConfidenceEventListener(TransactionEventListener transactionEventListener){ + if (this.mTransactionConfidenceListener != null) + vWallet.removeTransactionConfidenceEventListener(mTransactionConfidenceListener); + + transactionEventListener.setWallet(this); + vWallet.addTransactionConfidenceEventListener(transactionEventListener); + + mTransactionConfidenceListener = transactionEventListener; + } + + public TransactionEventListener getCoinsReceivedEventListener(){ + return this.mCoinsReceivedEventListener; + } + + /**

Retrieve the relevant address (P2PKH or P2PSH) and compares it with the notification address of this wallet.

*/ + public boolean isNotificationTransaction(Transaction tx) { + Address address = getAddressOfReceived(tx); + Address myNotificationAddress = mAccounts.get(0).getNotificationAddress(); + + return address != null && address.toString().equals(myNotificationAddress.toString()); + } + + /**

Retrieve the relevant address (P2PKH or P2PSH), return true if any key in this wallet translates to it.

*/ + // TODO: return true if and only if it is a channel address. + public boolean isToBIP47Address(Transaction transaction) { + List keys = vWallet.getImportedKeys(); + for (ECKey key : keys) { + Address address = key.toAddress(getParams()); + if (address == null) { + continue; + } + Address addressOfReceived = getAddressOfReceived(transaction); + if (addressOfReceived != null && address.toString().equals(addressOfReceived.toString())) { + return true; + } + } + return false; + } + + /** Find the address that received the transaction (P2PKH or P2PSH output) */ + public Address getAddressOfReceived(Transaction tx) { + for (final TransactionOutput output : tx.getOutputs()) { + try { + if (output.isMineOrWatched(vWallet)) { + final Script script = output.getScriptPubKey(); + return script.getToAddress(params, true); + } + } catch (final ScriptException x) { + // swallow + } + } + + return null; + } + + /* Find the address (in P2PKH or P2PSH output) that does not belong to this wallet. */ + public Address getAddressOfSent(Transaction tx) { + for (final TransactionOutput output : tx.getOutputs()) { + try { + if (!output.isMineOrWatched(vWallet)) { + final Script script = output.getScriptPubKey(); + return script.getToAddress(params, true); + } + } catch (final ScriptException x) { + // swallow + } + } + + return null; + } + + /** Given a notification transaction, extracts a valid payment code */ + public BIP47PaymentCode getPaymentCodeInNotificationTransaction(Transaction tx) { + byte[] privKeyBytes = mAccounts.get(0).getNotificationKey().getPrivKeyBytes(); + + return BIP47Util.getPaymentCodeInNotificationTransaction(privKeyBytes, tx); + } + + //

Receives a payment code and returns true iff there is already an incoming address generated for the channel

+ public boolean savePaymentCode(BIP47PaymentCode BIP47PaymentCode) { + if (bip47MetaData.containsKey(BIP47PaymentCode.toString())) { + BIP47Channel BIP47Channel = bip47MetaData.get(BIP47PaymentCode.toString()); + if (BIP47Channel.getIncomingAddresses().size() != 0) { + return false; + } else { + try { + BIP47Channel.generateKeys(this); + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + } + + BIP47Channel BIP47Channel = new BIP47Channel(BIP47PaymentCode.toString()); + + try { + BIP47Channel.generateKeys(this); + bip47MetaData.put(BIP47PaymentCode.toString(), BIP47Channel); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + + return true; + } + + public BIP47Account getAccount(int i) { + return mAccounts.get(i); + } + + public NetworkParameters getParams() { + return this.params; + } + + public Address getAddressOfKey(ECKey key) { + return key.toAddress(getParams()); + } + + public void importKey(ECKey key) { + vWallet.importKey(key); + } + + /** Return true if this is the first time the address is seen used*/ + public boolean generateNewBip47IncomingAddress(String address) { + for (BIP47Channel BIP47Channel : bip47MetaData.values()) { + for (BIP47Address bip47Address : BIP47Channel.getIncomingAddresses()) { + if (!bip47Address.getAddress().equals(address)) { + continue; + } + if (bip47Address.isSeen()) { + return false; + } + + int nextIndex = BIP47Channel.getCurrentIncomingIndex() + 1; + try { + ECKey key = getReceiveAddress(this, BIP47Channel.getPaymentCode(), nextIndex).getReceiveECKey(); + vWallet.importKey(key); + Address newAddress = getAddressOfKey(key); + BIP47Channel.addNewIncomingAddress(newAddress.toString(), nextIndex); + bip47Address.setSeen(true); + return true; + } catch (Exception e) { + e.printStackTrace(); + } + return false; + } + } + return false; + } + + public BIP47Channel getBip47MetaForAddress(String address) { + for (BIP47Channel BIP47Channel : bip47MetaData.values()) { + for (BIP47Address bip47Address : BIP47Channel.getIncomingAddresses()) { + if (bip47Address.getAddress().equals(address)) { + return BIP47Channel; + } + } + } + return null; + } + + public String getPaymentCodeForAddress(String address) { + for (BIP47Channel BIP47Channel : bip47MetaData.values()) { + for (BIP47Address bip47Address : BIP47Channel.getIncomingAddresses()) { + if (bip47Address.getAddress().equals(address)) { + return BIP47Channel.getPaymentCode(); + } + } + } + return null; + } + + public BIP47Channel getBip47MetaForOutgoingAddress(String address) { + for (BIP47Channel BIP47Channel : bip47MetaData.values()) { + for (String outgoingAddress : BIP47Channel.getOutgoingAddresses()) { + if (outgoingAddress.equals(address)) { + return BIP47Channel; + } + } + } + return null; + } + + public BIP47Channel getBip47MetaForPaymentCode(String paymentCode) { + for (BIP47Channel BIP47Channel : bip47MetaData.values()) { + if (BIP47Channel.getPaymentCode().equals(paymentCode)) { + return BIP47Channel; + } + } + return null; + } + + public Coin getValueOfTransaction(Transaction transaction) { + return transaction.getValue(vWallet); + } + + public Coin getValueSentToMe(Transaction transaction) { + return transaction.getValueSentToMe(vWallet); + } + + public Coin getValueSentFromMe(Transaction transaction) { + return transaction.getValueSentFromMe(vWallet); + } + + public List getTransactions() { + return vWallet.getTransactionsByTime(); + } + + public long getBalanceValue() { + return vWallet.getBalance(org.bitcoinj.wallet.Wallet.BalanceType.ESTIMATED_SPENDABLE).getValue(); + } + + public Coin getBalance() { + return vWallet.getBalance(org.bitcoinj.wallet.Wallet.BalanceType.ESTIMATED_SPENDABLE); + } + + public boolean isDownloading() { + return mBlockchainDownloadProgressTracker != null && mBlockchainDownloadProgressTracker.isDownloading(); + } + + public int getBlockchainProgress() { + return mBlockchainDownloadProgressTracker != null ? mBlockchainDownloadProgressTracker.getProgress() : -1; + } + + public boolean isTransactionEntirelySelf(Transaction tx) { + for (final TransactionInput input : tx.getInputs()) { + final TransactionOutput connectedOutput = input.getConnectedOutput(); + if (connectedOutput == null || !connectedOutput.isMine(vWallet)) + return false; + } + + for (final TransactionOutput output : tx.getOutputs()) { + if (!output.isMine(vWallet)) + return false; + } + + return true; + } + + public String getPaymentCode() { + return getAccount(0).getStringPaymentCode(); + } + + public void resetBlockchainSync() { + File chainFile = new File(directory, coinName+".spvchain"); + if (chainFile.exists()) { + log.debug("deleteSpvFile: exits"); + chainFile.delete(); + } + } + + public String getMnemonicCode() { + return join(this.restoreFromSeed != null ? + this.restoreFromSeed.getMnemonicCode() : + vWallet.getKeyChainSeed().getMnemonicCode()); + } + + public Address getCurrentAddress() { + return vWallet.currentReceiveAddress(); + } + + public Address getAddressFromBase58(String addr) { + return Address.fromBase58(getParams(), addr); + } + + /**

Returns true if the given address is a valid payment code or a valid address in the + * wallet's blockchain network.

*/ + public boolean isValidAddress(String address) { + if (address == null) + return false; + + try { + BIP47PaymentCode BIP47PaymentCode = new BIP47PaymentCode(address); + return true; + } catch (AddressFormatException e){ + } + + try { + Address.fromBase58(getParams(), address); + return true; + } catch (AddressFormatException e) { + try { + CashAddressFactory.create().getFromFormattedAddress(params, address); + return true; + } catch (AddressFormatException e2) { + return false; + } + } + } + + public Transaction createSend(String strAddr, long amount) throws InsufficientMoneyException { + Address address; + try { + address = Address.fromBase58(getParams(), strAddr); + } catch (AddressFormatException e1) { + try { + address = CashAddressFactory.create().getFromFormattedAddress(params, strAddr); + } catch (AddressFormatException e2) { + return null; + } + } + return createSend(address, amount); + } + + private static Coin getDefaultFee(NetworkParameters params){ + return Transaction.DEFAULT_TX_FEE; + } + + public Transaction createSend(Address address, long amount) throws InsufficientMoneyException { + SendRequest sendRequest = SendRequest.to(address, Coin.valueOf(amount)); + + sendRequest.feePerKb = getDefaultFee(getParams()); + + vWallet.completeTx(sendRequest); + return sendRequest.tx; + } + + public SendRequest makeNotificationTransaction(String paymentCode) throws InsufficientMoneyException { + BIP47Account toAccount = new BIP47Account(getParams(), paymentCode); + Coin ntValue = getParams().getMinNonDustOutput(); + Address ntAddress = toAccount.getNotificationAddress(); + + + log.debug("Balance: " + vWallet.getBalance()); + log.debug("To notification address: "+ntAddress.toString()); + log.debug("Value: "+ntValue.toFriendlyString()); + + SendRequest sendRequest = SendRequest.to(ntAddress, ntValue); + + sendRequest.feePerKb = getDefaultFee(getParams()); + + sendRequest.memo = "notification_transaction"; + + org.bitcoinj.utils.BIP47Util.FeeCalculation feeCalculation = BIP47Util.calculateFee(vWallet, sendRequest, ntValue, vWallet.calculateAllSpendCandidates()); + + for (TransactionOutput output :feeCalculation.bestCoinSelection.gathered) { + sendRequest.tx.addInput(output); + } + + if (sendRequest.tx.getInputs().size() > 0) { + TransactionInput txIn = sendRequest.tx.getInput(0); + RedeemData redeemData = txIn.getConnectedRedeemData(vWallet); + checkNotNull(redeemData, "StashTransaction exists in wallet that we cannot redeem: %s", txIn.getOutpoint().getHash()); + log.debug("Keys: "+redeemData.keys.size()); + log.debug("Private key 0?: "+redeemData.keys.get(0).hasPrivKey()); + byte[] privKey = redeemData.getFullKey().getPrivKeyBytes(); + log.debug("Private key: "+ Utils.HEX.encode(privKey)); + byte[] pubKey = toAccount.getNotificationKey().getPubKey(); + log.debug("Public Key: "+Utils.HEX.encode(pubKey)); + byte[] outpoint = txIn.getOutpoint().bitcoinSerialize(); + + byte[] mask = null; + try { + BIP47SecretPoint BIP47SecretPoint = new BIP47SecretPoint(privKey, pubKey); + log.debug("Secret Point: "+Utils.HEX.encode(BIP47SecretPoint.ECDHSecretAsBytes())); + log.debug("Outpoint: "+Utils.HEX.encode(outpoint)); + mask = BIP47PaymentCode.getMask(BIP47SecretPoint.ECDHSecretAsBytes(), outpoint); + } catch (Exception e) { + e.printStackTrace(); + } + log.debug("My payment code: "+mAccounts.get(0).getPaymentCode().toString()); + log.debug("Mask: "+Utils.HEX.encode(mask)); + byte[] op_return = BIP47PaymentCode.blind(mAccounts.get(0).getPaymentCode().getPayload(), mask); + + sendRequest.tx.addOutput(Coin.ZERO, ScriptBuilder.createOpReturnScript(op_return)); + } + + vWallet.completeTx(sendRequest); + + log.debug("Completed SendRequest"); + log.debug(sendRequest.toString()); + log.debug(sendRequest.tx.toString()); + + sendRequest.tx.verify(); + + return sendRequest; + } + + public Transaction getSignedNotificationTransaction(SendRequest sendRequest, String paymentCode) { + //BIP47Account toAccount = new BIP47Account(getParams(), paymentCode); + + // notification address pub key + //BIP47Util.signTransaction(vWallet, sendRequest, toAccount.getNotificationKey().getPubKey(), mAccounts.get(0).getPaymentCode()); + + vWallet.commitTx(sendRequest.tx); + + return sendRequest.tx; + } + + public ListenableFuture broadcastTransaction(Transaction transactionToSend) { + vWallet.commitTx(transactionToSend); + return vPeerGroup.broadcastTransaction(transactionToSend).future(); + } + + public boolean putBip47Meta(String profileId, String name, @Nullable Transaction ntx) { + if (bip47MetaData.containsKey(profileId)) { + BIP47Channel BIP47Channel = bip47MetaData.get(profileId); + if (ntx != null) + BIP47Channel.setNtxHash(ntx.getHash()); + if (!name.equals(BIP47Channel.getLabel())) { + BIP47Channel.setLabel(name); + return true; + } + } else { + bip47MetaData.put(profileId, new BIP47Channel(profileId, name)); + if (ntx != null) + bip47MetaData.get(profileId).setNtxHash(ntx.getHash()); + return true; + } + return false; + } + + /* Mark a channel's notification transaction as sent*/ + public void putPaymenCodeStatusSent(String paymentCode, Transaction ntx) { + if (bip47MetaData.containsKey(paymentCode)) { + BIP47Channel BIP47Channel = bip47MetaData.get(paymentCode); + BIP47Channel.setNtxHash(ntx.getHash()); + BIP47Channel.setStatusSent(); + } else { + putBip47Meta(paymentCode, paymentCode, ntx); + putPaymenCodeStatusSent(paymentCode, ntx); + } + } + + /* Return the next address to send a payment to */ + public String getCurrentOutgoingAddress(BIP47Channel BIP47Channel) { + try { + ECKey key = getSendAddress(this, new BIP47PaymentCode(BIP47Channel.getPaymentCode()), BIP47Channel.getCurrentOutgoingIndex()).getSendECKey(); + return key.toAddress(getParams()).toString(); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + public void commitTx(Transaction tx) { + vWallet.commitTx(tx); + } + + public org.bitcoinj.wallet.Wallet.SendResult sendCoins(SendRequest sendRequest) throws InsufficientMoneyException { + return vWallet.sendCoins(sendRequest); + } + + public void rescanTxBlock(Transaction tx) throws BlockStoreException { + int blockHeight = tx.getConfidence().getAppearedAtChainHeight() - 2; + this.vChain.rollbackBlockStore(blockHeight); + } + + public File getDirectory() { + return directory; + } + + public File getvWalletFile(){ + return this.vWalletFile; + } + + public PeerGroup getPeerGroup() { return this.vPeerGroup; } + + public org.bitcoinj.wallet.Wallet getvWallet(){ + return vWallet; + } + + public void closeBlockStore() throws BlockStoreException, IllegalAccessException { + if (isStarted()) + throw new IllegalAccessException("Must call stop() before closing block store"); + + if (vStore != null) { + vStore.close(); + } + } + + public List getAddresses(int size) { + List deterministicKeys = vWallet.getActiveKeyChain().getLeafKeys(); + List addresses = new ArrayList(size); + for (int i = 0; i < size; i++) { + addresses.add(deterministicKeys.get(i).toAddress(getParams()).toBase58()); + } + return addresses; + } + + public int getExternalAddressCount() { + return vWallet.getActiveKeyChain().getIssuedReceiveKeys().size(); + } + +} diff --git a/core/src/main/java/org/bitcoinj/utils/BIP47Util.java b/core/src/main/java/org/bitcoinj/utils/BIP47Util.java new file mode 100644 index 00000000000..d2c98363415 --- /dev/null +++ b/core/src/main/java/org/bitcoinj/utils/BIP47Util.java @@ -0,0 +1,419 @@ +/* Copyright (c) 2017 Stash + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.bitcoinj.utils; + +import org.bitcoinj.core.*; +import org.bitcoinj.core.bip47.BIP47PaymentAddress; +import org.bitcoinj.core.bip47.BIP47PaymentCode; +import org.bitcoinj.crypto.BIP47SecretPoint; +import org.bitcoinj.kits.BIP47AppKit; +import org.bitcoinj.script.Script; +import org.bitcoinj.script.ScriptBuilder; +import org.bitcoinj.script.ScriptChunk; +import org.bitcoinj.signers.MissingSigResolutionSigner; +import org.bitcoinj.signers.TransactionSigner; +import org.bitcoinj.wallet.CoinSelection; +import org.bitcoinj.wallet.CoinSelector; +import org.bitcoinj.wallet.DecryptingKeyBag; +import org.bitcoinj.wallet.KeyBag; +import org.bitcoinj.wallet.RedeemData; +import org.bitcoinj.wallet.SendRequest; +import org.bitcoinj.wallet.bip47.NotSecp256k1Exception; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.spec.InvalidKeySpecException; +import java.util.LinkedList; +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static org.bitcoinj.core.Utils.HEX; + +/** + * Created by jimmy on 10/3/17. + */ + +public class BIP47Util { + private static final String TAG = "BIP47Util"; + private static final Logger log = LoggerFactory.getLogger(BIP47Util.class); + + public static class FeeCalculation { + public CoinSelection bestCoinSelection; + public TransactionOutput bestChangeOutput; + } + + public static FeeCalculation calculateFee(org.bitcoinj.wallet.Wallet vWallet, SendRequest req, Coin value, List candidates) throws InsufficientMoneyException { + CoinSelector selector = vWallet.getCoinSelector(); + // There are 3 possibilities for what adding change might do: + // 1) No effect + // 2) Causes increase in fee (change < 0.01 COINS) + // 3) Causes the transaction to have a dust output or change < fee increase (ie change will be thrown away) + // If we get either of the last 2, we keep note of what the inputs looked like at the time and try to + // add inputs as we go up the list (keeping track of minimum inputs for each category). At the end, we pick + // the best input set as the one which generates the lowest total fee. + Coin additionalValueForNextCategory = null; + CoinSelection selection3 = null; + CoinSelection selection2 = null; + TransactionOutput selection2Change = null; + CoinSelection selection1 = null; + TransactionOutput selection1Change = null; + // We keep track of the last size of the transaction we calculated but only if the act of adding inputs and + // change resulted in the size crossing a 1000 byte boundary. Otherwise it stays at zero. + int lastCalculatedSize = 0; + Coin valueNeeded, valueMissing = null; + + Coin referenceDefaultMinTxFee = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE; + + while (true) { + req.tx.clearInputs(); + Coin fees = req.feePerKb.multiply(lastCalculatedSize).divide(1000); + if (fees.compareTo(referenceDefaultMinTxFee) < 0) + fees = referenceDefaultMinTxFee; + + valueNeeded = value.add(fees); + if (additionalValueForNextCategory != null) + valueNeeded = valueNeeded.add(additionalValueForNextCategory); + Coin additionalValueSelected = additionalValueForNextCategory; + + // Of the coins we could spend, pick some that we actually will spend. + // selector is allowed to modify candidates list. + CoinSelection selection = selector.select(valueNeeded, new LinkedList(candidates)); + // Can we afford this? + if (selection.valueGathered.compareTo(valueNeeded) < 0) { + valueMissing = valueNeeded.subtract(selection.valueGathered); + break; + } + + // We keep track of an upper bound on transaction size to calculate fees that need to be added. + // Note that the difference between the upper bound and lower bound is usually small enough that it + // will be very rare that we pay a fee we do not need to. + // + // We can't be sure a selection is valid until we check fee per kb at the end, so we just store + // them here temporarily. + boolean eitherCategory2Or3 = false; + boolean isCategory3 = false; + + Coin change = selection.valueGathered.subtract(valueNeeded); + if (additionalValueSelected != null) + change = change.add(additionalValueSelected); + + // If change is < 0.01 BTC, we will need to have at least minfee to be accepted by the network + if (req.ensureMinRequiredFee && !change.equals(Coin.ZERO) && + change.compareTo(Coin.CENT) < 0 && fees.compareTo(referenceDefaultMinTxFee) < 0) { + // This solution may fit into category 2, but it may also be category 3, we'll check that later + eitherCategory2Or3 = true; + additionalValueForNextCategory = Coin.CENT; + // If the change is smaller than the fee we want to add, this will be negative + change = change.subtract(referenceDefaultMinTxFee.subtract(fees)); + } + + int size = 0; + TransactionOutput changeOutput = null; + if (change.signum() > 0) { + // The value of the inputs is greater than what we want to send. Just like in real life then, + // we need to take back some coins ... this is called "change". Add another output that sends the change + // back to us. The address comes either from the request or currentChangeAddress() as a default. + Address changeAddress = req.changeAddress; + if (changeAddress == null) + changeAddress = vWallet.currentChangeAddress(); + changeOutput = new TransactionOutput(vWallet.getNetworkParameters(), req.tx, change, changeAddress); + // If the change output would result in this transaction being rejected as dust, just drop the change and make it a fee + if (req.ensureMinRequiredFee && changeOutput.isDust()) { + // This solution definitely fits in category 3 + isCategory3 = true; + additionalValueForNextCategory = referenceDefaultMinTxFee.add( + changeOutput.getMinNonDustValue().add(Coin.SATOSHI)); + } else { + size += changeOutput.unsafeBitcoinSerialize().length + VarInt.sizeOf(req.tx.getOutputs().size()) - VarInt.sizeOf(req.tx.getOutputs().size() - 1); + // This solution is either category 1 or 2 + if (!eitherCategory2Or3) // must be category 1 + additionalValueForNextCategory = null; + } + } else { + if (eitherCategory2Or3) { + // This solution definitely fits in category 3 (we threw away change because it was smaller than MIN_TX_FEE) + isCategory3 = true; + additionalValueForNextCategory = referenceDefaultMinTxFee.add(Coin.SATOSHI); + } + } + + // Now add unsigned inputs for the selected coins. + for (TransactionOutput output : selection.gathered) { + TransactionInput input = req.tx.addInput(output); + // If the scriptBytes don't default to none, our size calculations will be thrown off. + checkState(input.getScriptBytes().length == 0); + } + + // Estimate transaction size and loop again if we need more fee per kb. The serialized tx doesn't + // include things we haven't added yet like input signatures/scripts or the change output. + size += req.tx.unsafeBitcoinSerialize().length; + size += estimateBytesForSigning(vWallet, selection); + if (size > lastCalculatedSize && req.feePerKb.signum() > 0) { + lastCalculatedSize = size; + // We need more fees anyway, just try again with the same additional value + additionalValueForNextCategory = additionalValueSelected; + continue; + } + + if (isCategory3) { + if (selection3 == null) + selection3 = selection; + } else if (eitherCategory2Or3) { + // If we are in selection2, we will require at least CENT additional. If we do that, there is no way + // we can end up back here because CENT additional will always get us to 1 + checkState(selection2 == null); + checkState(additionalValueForNextCategory.equals(Coin.CENT)); + selection2 = selection; + selection2Change = checkNotNull(changeOutput); // If we get no change in category 2, we are actually in category 3 + } else { + // Once we get a category 1 (change kept), we should break out of the loop because we can't do better + checkState(selection1 == null); + checkState(additionalValueForNextCategory == null); + selection1 = selection; + selection1Change = changeOutput; + } + + if (additionalValueForNextCategory != null) { + if (additionalValueSelected != null) + checkState(additionalValueForNextCategory.compareTo(additionalValueSelected) > 0); + continue; + } + break; + } + + req.tx.clearInputs(); + + if (selection3 == null && selection2 == null && selection1 == null) { + checkNotNull(valueMissing); + log.warn("Insufficient value in wallet for send: needed "+valueMissing.toFriendlyString()+" more"); + throw new InsufficientMoneyException(valueMissing); + } + + Coin lowestFee = null; + FeeCalculation result = new FeeCalculation(); + if (selection1 != null) { + if (selection1Change != null) + lowestFee = selection1.valueGathered.subtract(selection1Change.getValue()); + else + lowestFee = selection1.valueGathered; + result.bestCoinSelection = selection1; + result.bestChangeOutput = selection1Change; + } + + if (selection2 != null) { + Coin fee = selection2.valueGathered.subtract(checkNotNull(selection2Change).getValue()); + if (lowestFee == null || fee.compareTo(lowestFee) < 0) { + lowestFee = fee; + result.bestCoinSelection = selection2; + result.bestChangeOutput = selection2Change; + } + } + + if (selection3 != null) { + if (lowestFee == null || selection3.valueGathered.compareTo(lowestFee) < 0) { + result.bestCoinSelection = selection3; + result.bestChangeOutput = null; + } + } + return result; + } + + /** + *

Given a send request containing transaction, attempts to sign it's inputs. This method expects transaction + * to have all necessary inputs connected or they will be ignored.

+ *

Actual signing is done by pluggable signers and it's not guaranteed that + * transaction will be complete in the end.

+ */ + static void signTransaction(org.bitcoinj.wallet.Wallet vWallet, SendRequest req, byte[] pubKey, BIP47PaymentCode myBIP47PaymentCode) { + Transaction tx = req.tx; + List inputs = tx.getInputs(); + List outputs = tx.getOutputs(); + checkState(inputs.size() > 0); + checkState(outputs.size() > 0); + + KeyBag maybeDecryptingKeyBag = new DecryptingKeyBag(vWallet, req.aesKey); + + int numInputs = tx.getInputs().size(); + for (int i = 0; i < numInputs; i++) { + TransactionInput txIn = tx.getInput(i); + if (txIn.getConnectedOutput() == null) { + // Missing connected output, assuming already signed. + continue; + } + + try { + // We assume if its already signed, its hopefully got a SIGHASH type that will not invalidate when + // we sign missing pieces (to check this would require either assuming any signatures are signing + // standard output types or a way to get processed signatures out of script execution) + txIn.getScriptSig().correctlySpends(tx, i, txIn.getConnectedOutput().getScriptPubKey()); + continue; + } catch (ScriptException e) { + log.debug("Input contained an incorrect signature", e); + // Expected. + } + + Script scriptPubKey = txIn.getConnectedOutput().getScriptPubKey(); + RedeemData redeemData = txIn.getConnectedRedeemData(maybeDecryptingKeyBag); + checkNotNull(redeemData, "StashTransaction exists in wallet that we cannot redeem: %s", txIn.getOutpoint().getHash()); + Script scriptSig = scriptPubKey.createEmptyInputScript(redeemData.keys.get(0), redeemData.redeemScript); + txIn.setScriptSig(scriptSig); + if (i == 0) { + log.debug("Keys: "+redeemData.keys.size()); + log.debug("Private key 0?: "+redeemData.keys.get(0).hasPrivKey()); + byte[] privKey = redeemData.getFullKey().getPrivKeyBytes(); + log.debug("Private key: "+ HEX.encode(privKey)); + log.debug("Public Key: "+HEX.encode(pubKey)); + byte[] outpoint = txIn.getOutpoint().bitcoinSerialize(); + + byte[] mask = null; + try { + BIP47SecretPoint BIP47SecretPoint = new BIP47SecretPoint(privKey, pubKey); + log.debug("Secret Point: "+HEX.encode(BIP47SecretPoint.ECDHSecretAsBytes())); + log.debug("Outpoint: "+HEX.encode(outpoint)); + mask = BIP47PaymentCode.getMask(BIP47SecretPoint.ECDHSecretAsBytes(), outpoint); + } catch (InvalidKeyException e) { + e.printStackTrace(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch (NoSuchProviderException e) { + e.printStackTrace(); + } catch (InvalidKeySpecException e) { + e.printStackTrace(); + } + log.debug("My payment code: "+ myBIP47PaymentCode.toString()); + log.debug("Mask: "+HEX.encode(mask)); + byte[] op_return = BIP47PaymentCode.blind(myBIP47PaymentCode.getPayload(), mask); + + tx.addOutput(Coin.ZERO, ScriptBuilder.createOpReturnScript(op_return)); + } + } + + tx.shuffleOutputs(); + + TransactionSigner.ProposedTransaction proposal = new TransactionSigner.ProposedTransaction(tx, true); + for (TransactionSigner signer : vWallet.getTransactionSigners()) { + if (!signer.signInputs(proposal, maybeDecryptingKeyBag)) + log.debug(signer.getClass().getName()+" returned false for the tx"); + } + + // resolve missing sigs if any + new MissingSigResolutionSigner(req.missingSigsMode).signInputs(proposal, maybeDecryptingKeyBag); + } + + private static int estimateBytesForSigning(org.bitcoinj.wallet.Wallet vWallet, CoinSelection selection) { + int size = 0; + for (TransactionOutput output : selection.gathered) { + try { + Script script = output.getScriptPubKey(); + ECKey key = null; + Script redeemScript = null; + if (script.isSentToAddress()) { + key = vWallet.findKeyFromPubHash(script.getPubKeyHash()); + checkNotNull(key, "Coin selection includes unspendable outputs"); + } else if (script.isPayToScriptHash()) { + redeemScript = vWallet.findRedeemDataFromScriptHash(script.getPubKeyHash()).redeemScript; + checkNotNull(redeemScript, "Coin selection includes unspendable outputs"); + } + size += script.getNumberOfBytesRequiredToSpend(key, redeemScript); + } catch (ScriptException e) { + // If this happens it means an output script in a wallet tx could not be understood. That should never + // happen, if it does it means the wallet has got into an inconsistent state. + throw new IllegalStateException(e); + } + } + return size; + } + + /** + * Finds the first output in a transaction whose op code is OP_RETURN. + */ + @Nullable + public static TransactionOutput getOpCodeOutput(Transaction tx) { + List outputs = tx.getOutputs(); + for (TransactionOutput o : outputs) { + if (o.getScriptPubKey().isOpReturn()) { + return o; + } + } + return null; + } + + /** Returns true if the OP_RETURN op code begins with the byte 0x01 (version 1), */ + public static boolean isValidNotificationTransactionOpReturn(TransactionOutput transactionOutput) { + byte[] data = getOpCodeData(transactionOutput); + return data != null && HEX.encode(data, 0, 1).equals("01"); + } + + /** Return the payload of the first op code e.g. OP_RETURN. */ + private static byte[] getOpCodeData(TransactionOutput opReturnOutput) { + List chunks = opReturnOutput.getScriptPubKey().getChunks(); + for (ScriptChunk chunk : chunks) { + if (!chunk.isOpCode() && chunk.data != null) { + return chunk.data; + } + } + return null; + } + + /* Extract the payment code from an incoming notification transaction */ + public static BIP47PaymentCode getPaymentCodeInNotificationTransaction(byte[] privKeyBytes, Transaction tx) { + log.debug( "Getting pub key"); + byte[] pubKeyBytes = tx.getInput(0).getScriptSig().getPubKey(); + + log.debug( "Private Key: "+ HEX.encode(privKeyBytes)); + log.debug( "Public Key: "+HEX.encode(pubKeyBytes)); + + log.debug( "Getting op_code data"); + TransactionOutput opReturnOutput = getOpCodeOutput(tx); + if (opReturnOutput == null) { + return null; + } + byte[] data = getOpCodeData(opReturnOutput); + + try { + log.debug( "Getting secret point.."); + BIP47SecretPoint BIP47SecretPoint = new BIP47SecretPoint(privKeyBytes, pubKeyBytes); + log.debug( "Secret Point: "+ HEX.encode(BIP47SecretPoint.ECDHSecretAsBytes())); + log.debug( "Outpoint: "+ HEX.encode(tx.getInput(0).getOutpoint().bitcoinSerialize())); + log.debug( "Getting mask..."); + byte[] s = BIP47PaymentCode.getMask(BIP47SecretPoint.ECDHSecretAsBytes(), tx.getInput(0).getOutpoint().bitcoinSerialize()); + log.debug( "Getting payload..."); + log.debug( "OpCode Data: "+HEX.encode(data)); + log.debug( "Mask: "+HEX.encode(s)); + byte[] payload = BIP47PaymentCode.blind(data, s); + log.debug( "Getting payment code..."); + BIP47PaymentCode BIP47PaymentCode = new BIP47PaymentCode(payload); + log.debug( "Payment Code: "+ BIP47PaymentCode.toString()); + return BIP47PaymentCode; + + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + /** Derives the receive address at idx in depositWallet for senderPaymentCode to deposit, in the wallet's bip47 0th account, i.e.
m / 47' / coin_type' / 0' / idx' .
. */ + public static BIP47PaymentAddress getReceiveAddress(BIP47AppKit depositWallet, String senderPaymentCode, int idx) throws AddressFormatException, NotSecp256k1Exception { + ECKey accountKey = depositWallet.getAccount(0).keyAt(idx); + return getPaymentAddress(depositWallet.getParams(), new BIP47PaymentCode(senderPaymentCode), 0, accountKey); + } + + /** Get the address of receiverBIP47PaymentCode's owner to send a payment to, using BTC as coin_type */ + public static BIP47PaymentAddress getSendAddress(BIP47AppKit spendWallet, BIP47PaymentCode receiverBIP47PaymentCode, int idx) throws AddressFormatException, NotSecp256k1Exception { + ECKey key = spendWallet.getAccount(0).keyAt(0); + return getPaymentAddress(spendWallet.getParams(), receiverBIP47PaymentCode, idx, key); + } + + /** Creates a BIP47PaymentAddress object that the sender will use to pay, using the hardened key at idx */ + private static BIP47PaymentAddress getPaymentAddress(NetworkParameters networkParameters, BIP47PaymentCode pcode, int idx, ECKey key) throws AddressFormatException, NotSecp256k1Exception { + return new BIP47PaymentAddress(networkParameters, pcode, idx, key.getPrivKeyBytes()); + } +} diff --git a/core/src/main/java/org/bitcoinj/wallet/bip47/NotSecp256k1Exception.java b/core/src/main/java/org/bitcoinj/wallet/bip47/NotSecp256k1Exception.java new file mode 100644 index 00000000000..cc28eaf30be --- /dev/null +++ b/core/src/main/java/org/bitcoinj/wallet/bip47/NotSecp256k1Exception.java @@ -0,0 +1,23 @@ +/* Copyright (c) 2017 Stash + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.bitcoinj.wallet.bip47; + +public class NotSecp256k1Exception extends Exception { + public NotSecp256k1Exception() { + } + + public NotSecp256k1Exception(String message) { + super(message); + } + + public NotSecp256k1Exception(String message, Throwable cause) { + super(message, cause); + } + + public NotSecp256k1Exception(Throwable cause) { + super(cause); + } +} diff --git a/core/src/main/java/org/bitcoinj/wallet/bip47/listeners/BlockchainDownloadProgressTracker.java b/core/src/main/java/org/bitcoinj/wallet/bip47/listeners/BlockchainDownloadProgressTracker.java new file mode 100644 index 00000000000..96a1eb7d42f --- /dev/null +++ b/core/src/main/java/org/bitcoinj/wallet/bip47/listeners/BlockchainDownloadProgressTracker.java @@ -0,0 +1,33 @@ +/* Copyright (c) 2017 Stash + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.bitcoinj.wallet.bip47.listeners; + +import org.bitcoinj.core.listeners.DownloadProgressTracker; + +/** + * Created by jimmy on 9/29/17. + */ + +public abstract class BlockchainDownloadProgressTracker extends DownloadProgressTracker { + protected boolean isDownloading = false; + private String mCoin; + + public BlockchainDownloadProgressTracker(String coin) { + super(); + + mCoin = coin; + } + + public String getCoin() { + return mCoin; + } + + public boolean isDownloading() { + return isDownloading; + } + + public abstract int getProgress(); +} diff --git a/core/src/main/java/org/bitcoinj/wallet/bip47/listeners/TransactionEventListener.java b/core/src/main/java/org/bitcoinj/wallet/bip47/listeners/TransactionEventListener.java new file mode 100644 index 00000000000..baac3584c55 --- /dev/null +++ b/core/src/main/java/org/bitcoinj/wallet/bip47/listeners/TransactionEventListener.java @@ -0,0 +1,39 @@ +/* Copyright (c) 2017 Stash + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.bitcoinj.wallet.bip47.listeners; + +import org.bitcoinj.kits.BIP47AppKit; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.listeners.TransactionConfidenceEventListener; +import org.bitcoinj.wallet.listeners.WalletCoinsReceivedEventListener; + +/** + * Created by jimmy on 9/29/17. + */ + +public abstract class TransactionEventListener implements WalletCoinsReceivedEventListener, TransactionConfidenceEventListener { + protected BIP47AppKit wallet; + + public void setWallet(BIP47AppKit wallet) { + this.wallet = wallet; + } + + @Override + public void onCoinsReceived(org.bitcoinj.wallet.Wallet wallet, Transaction transaction, Coin coin, Coin coin1) { + onTransactionReceived(this.wallet, transaction); + } + + @Override + public void onTransactionConfidenceChanged(org.bitcoinj.wallet.Wallet wallet, Transaction transaction) { + onTransactionConfidenceEvent(this.wallet, transaction); + } + + public abstract void onTransactionReceived(BIP47AppKit wallet, Transaction transaction); + + public abstract void onTransactionConfidenceEvent(BIP47AppKit wallet, Transaction transaction); +} diff --git a/core/src/test/java/org/bitcoinj/crypto/bip47/BIP47AccountTest.java b/core/src/test/java/org/bitcoinj/crypto/bip47/BIP47AccountTest.java new file mode 100644 index 00000000000..02dd67ff2a6 --- /dev/null +++ b/core/src/test/java/org/bitcoinj/crypto/bip47/BIP47AccountTest.java @@ -0,0 +1,40 @@ +package org.bitcoinj.crypto.bip47; + +import org.bitcoinj.core.AddressFormatException; +import org.bitcoinj.core.bip47.BIP47Account; +import org.bitcoinj.params.MainNetParams; +import org.bitcoinj.params.TestNet3Params; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertEquals; + +public class BIP47AccountTest { + private static final Logger log = LoggerFactory.getLogger(BIP47AccountTest.class); + + private final String ALICE_PAYMENT_CODE_V1 = "PM8TJTLJbPRGxSbc8EJi42Wrr6QbNSaSSVJ5Y3E4pbCYiTHUskHg13935Ubb7q8tx9GVbh2UuRnBc3WSyJHhUrw8KhprKnn9eDznYGieTzFcwQRya4GA"; + private final String ALICE_NOTIFICATION_ADDRESS = "1JDdmqFLhpzcUwPeinhJbUPw4Co3aWLyzW"; + private final String ALICE_NOTIFICATION_TESTADDRESS = "mxjb4tLKWrRsG3sGSMfgRPcFvCPkVgM4td"; + + @Test + public void constructFromPaymentCode() throws Exception { + // a valid payment code + BIP47Account acc = new BIP47Account(MainNetParams.get(), ALICE_PAYMENT_CODE_V1); + assertEquals(acc.getStringPaymentCode(), ALICE_PAYMENT_CODE_V1); + assertEquals(ALICE_NOTIFICATION_ADDRESS, acc.getNotificationAddress().toString()); + + + BIP47Account testAcc = new BIP47Account(TestNet3Params.get(), ALICE_PAYMENT_CODE_V1); + assertEquals(testAcc.getStringPaymentCode(), ALICE_PAYMENT_CODE_V1); + assertEquals(ALICE_NOTIFICATION_TESTADDRESS, testAcc.getNotificationAddress().toString()); + + // invalid payment code + try { + BIP47Account badAcc = new BIP47Account(MainNetParams.get(), ALICE_PAYMENT_CODE_V1.substring(0, 10)); + } catch (AddressFormatException expected){ + assertTrue(expected.getMessage().equalsIgnoreCase("Checksum does not validate")); + } + } +} \ No newline at end of file diff --git a/core/src/test/java/org/bitcoinj/testing/TestWithBIP47AppKit.java b/core/src/test/java/org/bitcoinj/testing/TestWithBIP47AppKit.java new file mode 100644 index 00000000000..89547d93384 --- /dev/null +++ b/core/src/test/java/org/bitcoinj/testing/TestWithBIP47AppKit.java @@ -0,0 +1,14 @@ +package org.bitcoinj.testing; + +import org.bitcoinj.kits.BIP47AppKit; + +public class TestWithBIP47AppKit extends TestWithWallet { + @Override + public void setUp() throws Exception { + super.setUp(); + } + + public void setWallet(BIP47AppKit w){ + this.wallet = w.getvWallet(); + } +} diff --git a/core/src/test/java/org/bitcoinj/wallet/bip47/BIP47AppKitTest.java b/core/src/test/java/org/bitcoinj/wallet/bip47/BIP47AppKitTest.java new file mode 100644 index 00000000000..4dbe44188d6 --- /dev/null +++ b/core/src/test/java/org/bitcoinj/wallet/bip47/BIP47AppKitTest.java @@ -0,0 +1,275 @@ +package org.bitcoinj.wallet.bip47; + +import org.bitcoinj.core.*; +import org.bitcoinj.core.bip47.BIP47Channel; +import org.bitcoinj.crypto.MnemonicCode; +import org.bitcoinj.kits.BIP47AppKit; +import org.bitcoinj.params.MainNetParams; +import org.bitcoinj.params.TestNet3Params; +import org.bitcoinj.testing.TestWithBIP47AppKit; +import org.bitcoinj.utils.BIP47Util; +import org.bitcoinj.wallet.*; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.security.SecureRandom; +import java.security.Security; +import java.util.List; + +import static org.bitcoinj.core.Utils.HEX; +import static org.junit.Assert.*; + +import org.bitcoinj.crypto.MnemonicCodeTest; +import org.spongycastle.jce.provider.BouncyCastleProvider; + +public class BIP47AppKitTest extends TestWithBIP47AppKit { + private static final Logger log = LoggerFactory.getLogger(org.bitcoinj.wallet.WalletTest.class); + + // - test vectors + private final String ALICE_BIP39_MNEMONIC = "response seminar brave tip suit recall often sound stick owner lottery motion"; + private final String ALICE_BIP39_RAW_ENTROPY = "b7b8706d714d9166e66e7ed5b3c61048"; + private final String ALICE_BIP32_SEED = "64dca76abc9c6f0cf3d212d248c380c4622c8f93b2c425ec6a5567fd5db57e10d3e6f94a2f6af4ac2edb8998072aad92098db73558c323777abf5bd1082d970a"; + private final String ALICE_PAYMENT_CODE_V1 = "PM8TJTLJbPRGxSbc8EJi42Wrr6QbNSaSSVJ5Y3E4pbCYiTHUskHg13935Ubb7q8tx9GVbh2UuRnBc3WSyJHhUrw8KhprKnn9eDznYGieTzFcwQRya4GA"; + private final String ALICE_NOTIFICATION_ADDRESS = "1JDdmqFLhpzcUwPeinhJbUPw4Co3aWLyzW"; + + private final String BOB_BIP39_MNEMONIC = "reward upper indicate eight swift arch injury crystal super wrestle already dentist"; + private final String BOB_BIP39_RAW_ENTROPY = "b8bde1cba37dbc161d09aad9bfc81c9d"; + private final String BOB_BIP32_SEED = "87eaaac5a539ab028df44d9110defbef3797ddb805ca309f61a69ff96dbaa7ab5b24038cf029edec5235d933110f0aea8aeecf939ed14fc20730bba71e4b1110"; + private final String BOB_PAYMENT_CODE_V1 = "PM8TJS2JxQ5ztXUpBBRnpTbcUXbUHy2T1abfrb3KkAAtMEGNbey4oumH7Hc578WgQJhPjBxteQ5GHHToTYHE3A1w6p7tU6KSoFmWBVbFGjKPisZDbP97"; + private final String BOB_NOTIFICATION_ADDRESS = "1ChvUUvht2hUQufHBXF8NgLhW8SwE2ecGV"; + + private final String SHARED_SECRET_0 = "f5bb84706ee366052471e6139e6a9a969d586e5fe6471a9b96c3d8caefe86fef"; + private final String SHARED_SECRET_1 = "adfb9b18ee1c4460852806a8780802096d67a8c1766222598dc801076beb0b4d"; + private final String SHARED_SECRET_2 = "79e860c3eb885723bb5a1d54e5cecb7df5dc33b1d56802906762622fa3c18ee5"; + private final String SHARED_SECRET_3 = "d8339a01189872988ed4bd5954518485edebf52762bf698b75800ac38e32816d"; + private final String SHARED_SECRET_4 = "14c687bc1a01eb31e867e529fee73dd7540c51b9ff98f763adf1fc2f43f98e83"; + private final String SHARED_SECRET_5 = "725a8e3e4f74a50ee901af6444fb035cb8841e0f022da2201b65bc138c6066a2"; + private final String SHARED_SECRET_6 = "521bf140ed6fb5f1493a5164aafbd36d8a9e67696e7feb306611634f53aa9d1f"; + private final String SHARED_SECRET_7 = "5f5ecc738095a6fb1ea47acda4996f1206d3b30448f233ef6ed27baf77e81e46"; + private final String SHARED_SECRET_8 = "1e794128ac4c9837d7c3696bbc169a8ace40567dc262974206fcf581d56defb4"; + private final String SHARED_SECRET_9 = "fe36c27c62c99605d6cd7b63bf8d9fe85d753592b14744efca8be20a4d767c37"; + + // - keypairs M'/47'/0'/0'/0' .. M'/47'/0'/0'/2147483647'\ + + // - parameters to generate keys in ECDH. + private String ALICE_; + + + private final String CHANNEL_NTX = "010000000186f411ab1c8e70ae8a0795ab7a6757aea6e4d5ae1826fc7b8f00c597d500609c010000" + + "006b483045022100ac8c6dbc482c79e86c18928a8b364923c774bfdbd852059f6b3778f2319b59a7022029d7cc5724e2f41ab1fcf" + + "c0ba5a0d4f57ca76f72f19530ba97c860c70a6bf0a801210272d83d8a1fa323feab1c085157a0791b46eba34afb8bfbfaeb3a3fcc3" + + "f2c9ad8ffffffff0210270000000000001976a9148066a8e7ee82e5c5b9b7dc1765038340dc5420a988ac1027000000000000536a4" + + "c50010002063e4eb95e62791b06c50e1a3a942e1ecaaa9afbbeb324d16ae6821e091611fa96c0cf048f607fe51a0327f5e252897931" + + "1c78cb2de0d682c61e1180fc3d543b0000000000000000000000000000000000"; + + private final String CARLOS_BIP39_MNEMONIC = "fetch genuine seek want smile sea orient elbow basic where arrange display mask country walnut shuffle usage airport juice price grant scan wild alone"; + private final String CARLOS_PAYMENT_CODE = "PM8TJaWSfZYLLuJnXctJ8npNYrUr5UCeT6KGmayJ4ENDSqj7VZr7uyX9exCo5JA8mFLkeXPaHoCBKuMDpYFs3tdxP2UxNiHSsZtb1KkKSVQyiwFhdLTZ"; + + private final Address OTHER_ADDRESS = new ECKey().toAddress(PARAMS); + + + // - blockchains to test + public static final String[] SUPPORTED_COINS = { "BCH", "BTC", "tBCH", "tBTC" }; + + // - + + static { + // Adds a new provider, at a specified position + Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 2); + Security.addProvider(new BouncyCastleProvider()); + } + private BIP47AppKit createWallet(String coinName, NetworkParameters params, File workingDir, String mnemonic) throws Exception { + DeterministicSeed seed = null; + if (mnemonic != null) + seed = new DeterministicSeed(mnemonic, null, "", Utils.currentTimeSeconds()); + return new BIP47AppKit(coinName, params, workingDir, seed); + }; + + static void deleteFolder(String dirname){ + File dir = new File(dirname); + if (!dir.exists()) + return; + String[] entries = dir.list(); + for(String s: entries){ + File currentFile = new File(dir.getPath(),s); + if (currentFile.isDirectory()) + deleteFolder((currentFile.getAbsolutePath())); + else + currentFile.delete(); + } + dir.delete(); + } + + @Test + public void aliceWalletTest() throws Exception { + + // - test bip 39 + MnemonicCode mc = new MnemonicCode(); + List code = mc.toMnemonic(HEX.decode(ALICE_BIP39_RAW_ENTROPY)); + byte[] seed = MnemonicCode.toSeed(code,""); + byte[] entropy = mc.toEntropy(MnemonicCodeTest.split(ALICE_BIP39_MNEMONIC)); + + assertEquals(ALICE_BIP39_RAW_ENTROPY, HEX.encode(entropy)); + assertEquals(ALICE_BIP39_MNEMONIC, Utils.join(code)); + assertEquals(ALICE_BIP32_SEED, HEX.encode(seed)); + + File workingDir = new File("alice"); + + // - test bip 47 + BIP47AppKit w = createWallet("BCH", MainNetParams.get(),workingDir,ALICE_BIP39_MNEMONIC); + assertEquals("xpub6D3t231wUi5v9PEa8mgmyV7Tovg3CzrGEUGNQTfm9cK93je3PgX9udfhzUDx29pkeeHQBPpTSHpAxnDgsf2XRbvLrmbCUQybjtHx8SUb3JB", w.getAccount(0).getXPub()); + byte[] BCH_PUBKEY = w.getAccount(0).getPaymentCode().getPubKey(); + assertEquals(ALICE_PAYMENT_CODE_V1, w.getPaymentCode()); + assertEquals(ALICE_NOTIFICATION_ADDRESS, w.getAccount(0).getNotificationAddress().toString()); + } + + @Test + public void bobWalletTest() throws Exception { + // - test bip 39 + MnemonicCode mc = new MnemonicCode(); + List code = mc.toMnemonic(HEX.decode(BOB_BIP39_RAW_ENTROPY)); + byte[] seed = MnemonicCode.toSeed(code,""); + byte[] entropy = mc.toEntropy(MnemonicCodeTest.split(BOB_BIP39_MNEMONIC)); + assertEquals(BOB_BIP39_RAW_ENTROPY, HEX.encode(entropy)); + assertEquals(BOB_BIP39_MNEMONIC, Utils.join(code)); + assertEquals(BOB_BIP32_SEED, HEX.encode(seed)); + + File workingDir = new File("bob"); + + BIP47AppKit w = createWallet("BTC", MainNetParams.get(), workingDir, BOB_BIP39_MNEMONIC); + //assertEquals("tpubDCyvczNnKRM37QUHTCG1d6dFbXXkPUNfoay6XjVRhBKaGy47i1nFJQEmusyybMjaHBgpBbPFJRvwsWjtqQ8GTNiDw62ngm18w3QqyV6eHrY", w.getAccount(0).getXPub()); + assertEquals(BOB_PAYMENT_CODE_V1, w.getPaymentCode()); + + w = createWallet("tBTC", TestNet3Params.get(),workingDir,BOB_BIP39_MNEMONIC); + //assertEquals("tpubDDf84AmZb36BcJKZHrpuToRNj9bhDEwQ1PbrZ7guzq3EHFLxhW9ZjtghxLZauHVwXsm42wSRRxrNEkbFJu4qmvA1PyK8rYTa1o33XVsr6vw", w.getAccount(0).getXPub()); + assertEquals(BOB_PAYMENT_CODE_V1, w.getPaymentCode()); + //assertEquals(BOB_NOTIFICATION_ADDRESS, w.getAccount(0).getNotificationAddress().toString()); + } + + @Test + public void notificationTransactionTest() throws Exception { + super.setUp(); + // folders for alice and bob wallets + + deleteFolder("alice2");deleteFolder("bob2"); + File aliceDir = new File("alice2"); + File bobDir = new File("bob2"); + + BIP47AppKit Alice = createWallet("BTC", MainNetParams.get(), aliceDir, ALICE_BIP39_MNEMONIC); + BIP47AppKit Bob = createWallet("BTC", MainNetParams.get(), bobDir, BOB_BIP39_MNEMONIC); + + // Alice sends a payment to Bob, she saves Bob's payment code. + + setWallet(Alice); + assertTrue(Alice.getCoinsReceivedEventListener() != null); + assertTrue(Alice.getAccount(0) != null); + + // both have issued 1 receive address + assertEquals(1, Alice.getExternalAddressCount()); + assertEquals(1, Bob.getExternalAddressCount()); + assertEquals(0, Bob.getvWallet().getImportedKeys().size()); + + sendMoneyToWallet(Alice.getvWallet(), AbstractBlockChain.NewBlockType.BEST_CHAIN, Coin.COIN, Alice.getCurrentAddress()); + + SendRequest ntxRequest = Alice.makeNotificationTransaction(Bob.getPaymentCode()); + + // outpoint of first UTXO in Alice's NTX to bob' + //assertEquals("9414f1681fb1255bd168a806254321a837008dd4480c02226063183deb100204", ntxRequest.tx.getHash()); + + // Bob receives a NTX with Alice's payment code. Bob's wallet generates keys for Alice to use. + Bob.savePaymentCode(Alice.getAccount(0).getPaymentCode()); // bob saves alice + BIP47Channel channel = Bob.getBip47MetaForPaymentCode(Alice.getPaymentCode()); + assertEquals(10, channel.getIncomingAddresses().size()); // bob's # of incoming addresses + assertEquals(10, Bob.getvWallet().getImportedKeys().size()); + + // - addresses used by Alice for sending to Bob + assertEquals("141fi7TY3h936vRUKh1qfUZr8rSBuYbVBK", channel.getIncomingAddresses().get(0).getAddress()); + assertEquals("12u3Uued2fuko2nY4SoSFGCoGLCBUGPkk6", channel.getIncomingAddresses().get(1).getAddress()); + assertEquals("1FsBVhT5dQutGwaPePTYMe5qvYqqjxyftc", channel.getIncomingAddresses().get(2).getAddress()); + assertEquals("1CZAmrbKL6fJ7wUxb99aETwXhcGeG3CpeA", channel.getIncomingAddresses().get(3).getAddress()); + assertEquals("1KQvRShk6NqPfpr4Ehd53XUhpemBXtJPTL", channel.getIncomingAddresses().get(4).getAddress()); + assertEquals("1KsLV2F47JAe6f8RtwzfqhjVa8mZEnTM7t", channel.getIncomingAddresses().get(5).getAddress()); + assertEquals("1DdK9TknVwvBrJe7urqFmaxEtGF2TMWxzD", channel.getIncomingAddresses().get(6).getAddress()); + assertEquals("16DpovNuhQJH7JUSZQFLBQgQYS4QB9Wy8e", channel.getIncomingAddresses().get(7).getAddress()); + assertEquals("17qK2RPGZMDcci2BLQ6Ry2PDGJErrNojT5", channel.getIncomingAddresses().get(8).getAddress()); + assertEquals("1GxfdfP286uE24qLZ9YRP3EWk2urqXgC4s", channel.getIncomingAddresses().get(9).getAddress()); + + assertEquals(SHARED_SECRET_0, HEX.encode(BIP47Util.getReceiveAddress(Bob, ALICE_PAYMENT_CODE_V1, 0).getSharedSecret().ECDHSecretAsBytes())); + assertEquals(SHARED_SECRET_1, HEX.encode(BIP47Util.getReceiveAddress(Bob, ALICE_PAYMENT_CODE_V1, 1).getSharedSecret().ECDHSecretAsBytes())); + assertEquals(SHARED_SECRET_2, HEX.encode(BIP47Util.getReceiveAddress(Bob, ALICE_PAYMENT_CODE_V1, 2).getSharedSecret().ECDHSecretAsBytes())); + assertEquals(SHARED_SECRET_3, HEX.encode(BIP47Util.getReceiveAddress(Bob, ALICE_PAYMENT_CODE_V1, 3).getSharedSecret().ECDHSecretAsBytes())); + assertEquals(SHARED_SECRET_4, HEX.encode(BIP47Util.getReceiveAddress(Bob, ALICE_PAYMENT_CODE_V1, 4).getSharedSecret().ECDHSecretAsBytes())); + assertEquals(SHARED_SECRET_5, HEX.encode(BIP47Util.getReceiveAddress(Bob, ALICE_PAYMENT_CODE_V1, 5).getSharedSecret().ECDHSecretAsBytes())); + assertEquals(SHARED_SECRET_6, HEX.encode(BIP47Util.getReceiveAddress(Bob, ALICE_PAYMENT_CODE_V1, 6).getSharedSecret().ECDHSecretAsBytes())); + assertEquals(SHARED_SECRET_7, HEX.encode(BIP47Util.getReceiveAddress(Bob, ALICE_PAYMENT_CODE_V1, 7).getSharedSecret().ECDHSecretAsBytes())); + assertEquals(SHARED_SECRET_8, HEX.encode(BIP47Util.getReceiveAddress(Bob, ALICE_PAYMENT_CODE_V1, 8).getSharedSecret().ECDHSecretAsBytes())); + assertEquals(SHARED_SECRET_9, HEX.encode(BIP47Util.getReceiveAddress(Bob, ALICE_PAYMENT_CODE_V1, 9).getSharedSecret().ECDHSecretAsBytes())); + + + //assertEquals("736a25d9250238ad64ed5da03450c6a3f4f8f4dcdf0b58d1ed69029d76ead48d", HEX.encode(firstAddress.getSharedSecret().getPrivKey().getEncoded())); + //assertEquals("tpubDCyvczNnKRM37QUHTCG1d6dFbXXkPUNfoay6XjVRhBKaGy47i1nFJQEmusyybMjaHBgpBbPFJRvwsWjtqQ8GTNiDw62ngm18w3QqyV6eHrY", w.getAccount(0).getXPub()); + + } + + @Test + public void carlosWalletTest() throws Exception { + File workingDir = new File("carlos"); + + BIP47AppKit w = createWallet("tBTC", TestNet3Params.get(), workingDir,CARLOS_BIP39_MNEMONIC); + assertEquals("tpubDCfC54qrR5PkDXCL2TkCJ46pYbFt7CX3UDF9e7qxsQw8Nm9HQy7eZ7tL3FrHhJhxAZU8dwmqpzhntLxax93914cq8vQUTsAxcKPBBoZDm28", w.getAccount(0).getXPub()); + assertEquals(CARLOS_PAYMENT_CODE, w.getPaymentCode()); + + w = createWallet("BTC", MainNetParams.get(), workingDir,CARLOS_BIP39_MNEMONIC); + //assertEquals("tpubDDX2RK6EL7nuqjxFuZZTKsyMDx7PvPnbXmAtwuZaL9QorhjtussQTW5ReBF3G8G3wAY3RyusFkW2AuWz8YsiNXtkHZn2DmJRXA6m3rRwH8A", w.getAccount(0).getXPub()); + } + + @Test + public void testPeerGroupStart() throws Exception{ + BIP47AppKit w = new BIP47AppKit("tBTC", TestNet3Params.get(), new File("peerGroup"), null); + assertFalse(w.isStarted()); + assertFalse(w.isStarted()); + w.startBlockchainDownload(); + assertTrue(w.isStarted()); + w.startBlockchainDownload(); + assertTrue(w.isStarted()); + w.stop(); + assertFalse(w.isStarted()); + w.startBlockchainDownload(); + assertTrue(w.isStarted()); + } + + @Test + public void testIsValidAddress() throws Exception { + // test tbch + BIP47AppKit w = new BIP47AppKit("BCH", MainNetParams.get(), new File("validAdress"), null); + assertFalse(w.isValidAddress(null)); + assertFalse(w.isValidAddress("")); + assertTrue(w.isValidAddress(ALICE_PAYMENT_CODE_V1)); + assertTrue(w.isValidAddress("bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a")); + } + + /* Test that a wallet restored from seed is persistent */ + @Test + public void testMnemonicWordsPersistence() throws Exception{ + // create a fresh new wallet + String davesPath = "src/test/resources/org/bitcoinj/wallet/dave-bip47"; + File davesDir = new File(davesPath); + deleteFolder(davesPath); + DeterministicSeed davesSeed = new DeterministicSeed(new SecureRandom(), 256, "", System.currentTimeMillis() / 1000); + assertFalse(davesDir.exists()); //delete previous wallets created by this test + //create Dave's wallet and save it + BIP47AppKit Dave = new BIP47AppKit("BTC", MainNetParams.get(), davesDir, davesSeed); + String davesMnemonic = Dave.getMnemonicCode(); + String davesPaymentCode = Dave.getPaymentCode(); + assertTrue(davesDir.exists()); + Dave.stop(); + Dave.closeBlockStore(); + // the same directory/coin will have the same seed as saved before. + BIP47AppKit DaveReload = new BIP47AppKit("BTC", MainNetParams.get(), davesDir, null); + assertEquals(DaveReload.getMnemonicCode(), davesMnemonic); + assertEquals(DaveReload.getPaymentCode(), davesPaymentCode); + deleteFolder(davesPath); + } +} diff --git a/core/src/test/java/org/bitcoinj/wallet/bip47/BIP47PaymentCodeTest.java b/core/src/test/java/org/bitcoinj/wallet/bip47/BIP47PaymentCodeTest.java new file mode 100644 index 00000000000..76592aea04d --- /dev/null +++ b/core/src/test/java/org/bitcoinj/wallet/bip47/BIP47PaymentCodeTest.java @@ -0,0 +1,47 @@ +package org.bitcoinj.wallet.bip47; + +import org.bitcoinj.core.AddressFormatException; +import org.bitcoinj.core.bip47.BIP47Account; +import org.bitcoinj.core.bip47.BIP47PaymentCode; +import org.bitcoinj.params.MainNetParams; +import org.junit.Test; + +import static org.bitcoinj.core.Utils.HEX; +import static org.junit.Assert.assertEquals; + +public class BIP47PaymentCodeTest { + private final String ALICE_PAYMENT_CODE_V1 = "PM8TJTLJbPRGxSbc8EJi42Wrr6QbNSaSSVJ5Y3E4pbCYiTHUskHg13935Ubb7q8tx9GVbh2UuRnBc3WSyJHhUrw8KhprKnn9eDznYGieTzFcwQRya4GA"; + private final String ALICE_NOTIFICATION_ADDRESS = "1JDdmqFLhpzcUwPeinhJbUPw4Co3aWLyzW"; + private final String ALICE_NOTIFICATION_TESTADDRESS = "mxjb4tLKWrRsG3sGSMfgRPcFvCPkVgM4td"; + + @Test + public void pubKeyDeriveTests(){ + + BIP47PaymentCode alice = new BIP47PaymentCode(ALICE_PAYMENT_CODE_V1); + BIP47Account acc = new BIP47Account(MainNetParams.get(), ALICE_PAYMENT_CODE_V1); + + byte[] alice0th = alice.derivePubKeyAt(MainNetParams.get(),0); + byte[] acc0th = acc.getNotificationKey().getPubKey(); + + byte[] alice1st = alice.derivePubKeyAt(MainNetParams.get(),1); + byte[] acc1st = acc.keyAt(1).getPubKey(); + + assertEquals(HEX.encode(alice0th), HEX.encode(acc0th)); + assertEquals(HEX.encode(alice1st), HEX.encode(acc1st)); + } + + @Test(expected = AddressFormatException.class) + public void invalidPaymentCodeTest1(){ + BIP47PaymentCode invalid = new BIP47PaymentCode("XXXTJTLJbPRGxSbc8EJi42Wrr6QbNSaSSVJ5Y3E4pbCYiTHUskHg13935Ubb7q8tx9GVbh2UuRnBc3WSyJHhUrw8KhprKnn9eDznYGieTzFcwQRya4GA"); + } + + @Test(expected = AddressFormatException.class) + public void invalidPaymentCodeTest2(){ + BIP47PaymentCode invalid = new BIP47PaymentCode(""); + } + + @Test(expected = AddressFormatException.class) + public void invalidPaymentCodeTest3(){ + new BIP47PaymentCode(ALICE_PAYMENT_CODE_V1.replace('x','y')); + } +} \ No newline at end of file