// Copyright (C) 2013-2014 Bonsai Software, Inc. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see <http://www.gnu.org/licenses/>. package com.bonsai.wallet32; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import org.json.JSONException; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.spongycastle.crypto.params.KeyParameter; import com.google.bitcoin.core.Address; import com.google.bitcoin.core.AddressFormatException; import com.google.bitcoin.core.Base58; import com.google.bitcoin.core.ECKey; import com.google.bitcoin.core.NetworkParameters; import com.google.bitcoin.core.Utils; import com.google.bitcoin.core.Wallet; import com.google.bitcoin.crypto.DeterministicKey; import com.google.bitcoin.crypto.HDKeyDerivation; import com.google.bitcoin.crypto.KeyCrypter; public class HDAddress { // Tue Oct 15 11:18:03 PDT 2013 public static final long EPOCH = 1381861127; private static Logger mLogger = LoggerFactory.getLogger(HDAddress.class); private NetworkParameters mParams; private int mAddrNum; private String mPath; private byte[] mPrvBytes; private byte[] mPubBytes; private ECKey mECKey; private byte[] mPubKey; private byte[] mPubKeyHash; private Address mAddress; private int mNumTrans; private long mBalance; private long mAvailable; // Available for spending. public HDAddress(NetworkParameters params, DeterministicKey chainKey, JSONObject addrNode) throws RuntimeException, JSONException { mParams = params; mAddrNum = addrNode.getInt("addrNum"); // If our persisted state doesn't have the path or prvBytes // we'll need to use the expensive operation to derive them. // We'll persist them going forward so we can do the faster // deserialization. // if (!addrNode.has("path") || !addrNode.has("prvBytes")) { DeterministicKey dk = HDKeyDerivation.deriveChildKey(chainKey, mAddrNum); // Derive ECKey. mPrvBytes = dk.getPrivKeyBytes(); mPath = dk.getPath(); } else { try { mPrvBytes = Base58.decode(addrNode.getString("prvBytes")); } catch (AddressFormatException ex) { throw new RuntimeException("failed to decode prvBytes"); } mPath = addrNode.getString("path"); } try { mPubBytes = Base58.decode(addrNode.getString("pubBytes")); } catch (AddressFormatException ex) { throw new RuntimeException("failed to decode pubBytes"); } mECKey = new ECKey(mPrvBytes, mPubBytes); // Set creation time to Wallet32 epoch. mECKey.setCreationTimeSeconds(EPOCH); // Derive public key, public hash and address. mPubKey = mECKey.getPubKey(); mPubKeyHash = mECKey.getPubKeyHash(); mAddress = mECKey.toAddress(mParams); // Initialize transaction count and balance. If we don't have // a persisted available amount, presume it is all available. mNumTrans = addrNode.getInt("numTrans"); mBalance = addrNode.getLong("balance"); mAvailable = addrNode.has("available") ? addrNode.getLong("available") : mBalance; mLogger.info("read address " + mPath + ": " + mAddress.toString()); } public JSONObject dumps() { try { JSONObject obj = new JSONObject(); obj.put("addrNum", mAddrNum); obj.put("path", mPath); obj.put("prvBytes", Base58.encode(mPrvBytes)); obj.put("pubBytes", Base58.encode(mPubBytes)); obj.put("numTrans", mNumTrans); obj.put("balance", mBalance); obj.put("available", mAvailable); return obj; } catch (JSONException ex) { throw new RuntimeException(ex); // Shouldn't happen. } } public HDAddress(NetworkParameters params, DeterministicKey chainKey, int addrnum) { mParams = params; mAddrNum = addrnum; DeterministicKey dk = HDKeyDerivation.deriveChildKey(chainKey, addrnum); mPath = dk.getPath(); // Derive ECKey. mPrvBytes = dk.getPrivKeyBytes(); mPubBytes = dk.getPubKeyBytes(); // Expensive, save. mECKey = new ECKey(mPrvBytes, mPubBytes); // Set creation time to now. long now = Utils.now().getTime() / 1000; mECKey.setCreationTimeSeconds(now); // Derive public key, public hash and address. mPubKey = mECKey.getPubKey(); mPubKeyHash = mECKey.getPubKeyHash(); mAddress = mECKey.toAddress(mParams); // Initialize transaction count and balance. mNumTrans = 0; mBalance = 0; mAvailable = 0; mLogger.info("created address " + mPath + ": " + mAddress.toString()); } public void gatherKey(KeyCrypter keyCrypter, KeyParameter aesKey, long creationTime, List<ECKey> keys) { mECKey.setCreationTimeSeconds(creationTime); keys.add(mECKey.encrypt(keyCrypter, aesKey)); } public boolean isMatch(byte[] pubkey, byte[] pubkeyhash) { if (pubkey != null) return Arrays.equals(pubkey, mPubKey); else if (pubkeyhash != null) return Arrays.equals(pubkeyhash, mPubKeyHash); else return false; } public void applyOutput(byte[] pubkey, byte[] pubkeyhash, long value, boolean avail) { // Does this output apply to this address? if (!isMatch(pubkey, pubkeyhash)) return; ++mNumTrans; mBalance += value; if (avail) mAvailable += value; mLogger.debug(mPath + " matched output of " + Long.toString(value)); } public void applyInput(byte[] pubkey, long value) { // Does this input apply to this address? if (!Arrays.equals(pubkey, mPubKey)) return; ++mNumTrans; mBalance -= value; mAvailable -= value; mLogger.debug(mPath + " matched input of " + Long.toString(value)); } public String getPath() { return mPath; } public long getBalance() { return mBalance; } public long getAvailable() { return mAvailable; } public String getAddressString() { return mAddress.toString(); } public String getAbbrev() { return mAddress.toString().substring(0, 8) + "..."; } public String getPrivateKeyString() { return mECKey.getPrivateKeyEncoded(mParams).toString(); } public int numTrans() { return mNumTrans; } public void clearBalance() { mNumTrans = 0; mBalance = 0; mAvailable = 0; } public void logBalance() { if (mNumTrans > 0) { mLogger.info(mPath + " " + Integer.toString(mNumTrans) + " " + Long.toString(mBalance) + " " + Long.toString(mAvailable)); } } public boolean isUnused() { return mNumTrans == 0; } public Address getAddress() { return mAddress; } public boolean matchAddress(Address addr) { return mAddress.toString().equals(addr.toString()); } } // Local Variables: // mode: java // c-basic-offset: 4 // tab-width: 4 // End: