在上一篇文章中,我们已经初步实现了交易。相信你应该了解了交易中的一些天然属性,这些属性没有丝毫“个人”色彩的存在:在比特币中,没有用户账户,不需要也不会在任何地方存储个人数据(比如姓名,护照号码或者 SSN)。但是,我们总要有某种途径识别出你是交易输出的所有者(也就是说,你拥有在这些输出上锁定的币)。这就是比特币地址(address)需要完成的使命。在上一篇中,我们把一个由用户定义的任意字符串当成是地址,现在我们将要实现一个跟比特币一样的真实地址。
学会什么是Base58编码和解码
学会创建私钥和公钥
学会使用公钥生成钱包地址
学会将钱包地址进行持久化存储
打开IntelliJ IDEA的工作空间,将上一个项目代码目录part5_Transaction
,复制为part6_Wallet
。
然后打开IntelliJ IDEA开发工具。
打开工程:part6_Wallet
,并删除target目录。然后进行以下修改:
step1:先将项目重新命名为:part6_Wallet。
step2:修改pom.xml配置文件。
改为:<artifactId>part6_Wallet</artifactId>标签
改为:<name>part6_Wallet Maven Webapp</name>
说明:我们每一章节的项目代码,都是在上一个章节上进行添加。所以拷贝上一次的项目代码,然后进行新内容的添加或修改。
Base58Check.java
打开cldy.hanru.blockchain.util
包,新建Base58Check.java
文件。添加Base58的编码和解码方法,代码如下:
package cldy.hanru.blockchain.util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.util.Arrays;
/**
* Base58 转化工具
*
* @author hanru
*
*/
public final class Base58Check {
/*---- Class constants ----*/
private static final String ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
private static final BigInteger ALPHABET_SIZE = BigInteger.valueOf(ALPHABET.length());
/**
* 添加校验码并转化为 Base58 字符串
*
* @param data
* @return
*/
public static String bytesToBase58(byte[] data) {
return rawBytesToBase58(addCheckHash(data));
}
/**
* 转化为 Base58 字符串
*
* @param data
* @return
*/
public static String rawBytesToBase58(byte[] data) {
// Convert to base-58 string
StringBuilder sb = new StringBuilder();
BigInteger num = new BigInteger(1, data);
while (num.signum() != 0) {
BigInteger[] quotrem = num.divideAndRemainder(ALPHABET_SIZE);
sb.append(ALPHABET.charAt(quotrem[1].intValue()));
num = quotrem[0];
}
// Add '1' characters for leading 0-value bytes
for (int i = 0; i < data.length && data[i] == 0; i++) {
sb.append(ALPHABET.charAt(0));
}
return sb.reverse().toString();
}
/**
* 添加校验码并返回待有校验码的原生数据
*
* @param data
* @return
*/
static byte[] addCheckHash(byte[] data) {
try {
byte[] hash = Arrays.copyOf(AddressUtils.doubleHash(data), 4);
ByteArrayOutputStream buf = new ByteArrayOutputStream();
buf.write(data);
buf.write(hash);
return buf.toByteArray();
} catch (IOException e) {
throw new AssertionError(e);
}
}
/**
* 将 Base58Check 字符串转化为 byte 数组,并校验其校验码
* 返回的byte数组带有版本号,但不带有校验码
*
* @param s
* @return
*/
public static byte[] base58ToBytes(String s) {
byte[] concat = base58ToRawBytes(s);
byte[] data = Arrays.copyOf(concat, concat.length - 4);
byte[] hash = Arrays.copyOfRange(concat, concat.length - 4, concat.length);
byte[] rehash = Arrays.copyOf(AddressUtils.doubleHash(data), 4);
if (!Arrays.equals(rehash, hash)) {
throw new IllegalArgumentException("Checksum mismatch");
}
return data;
}
/**
* 将 Base58Check 字符串反转为 byte 数组
*
* @param s
* @return
*/
static byte[] base58ToRawBytes(String s) {
// Parse base-58 string
BigInteger num = BigInteger.ZERO;
for (int i = 0; i < s.length(); i++) {
num = num.multiply(ALPHABET_SIZE);
int digit = ALPHABET.indexOf(s.charAt(i));
if (digit == -1) {
throw new IllegalArgumentException("Invalid character for Base58Check");
}
num = num.add(BigInteger.valueOf(digit));
}
// Strip possible leading zero due to mandatory sign bit
byte[] b = num.toByteArray();
if (b[0] == 0) {
b = Arrays.copyOfRange(b, 1, b.length);
}
try {
// Convert leading '1' characters to leading 0-value bytes
ByteArrayOutputStream buf = new ByteArrayOutputStream();
for (int i = 0; i < s.length() && s.charAt(i) == ALPHABET.charAt(0); i++) {
buf.write(0);
}
buf.write(b);
return buf.toByteArray();
} catch (IOException e) {
throw new AssertionError(e);
}
}
/*---- Miscellaneous ----*/
private Base58Check() {
} // Not instantiable
}
AddressUtils.java
打开cldy.hanru.blockchain.util
包,新建AddressUtils.java
文件。在AddressUtils.java
文件中编写代码如下:
package cldy.hanru.blockchain.util;
import org.apache.commons.codec.digest.DigestUtils;
import org.bouncycastle.crypto.digests.RIPEMD160Digest;
import org.bouncycastle.util.Arrays;
/**
* 地址工具类
*
* @author hanru
*
*/
public class AddressUtils {
/**
* 双重Hash
*
* @param data
* @return
*/
public static byte[] doubleHash(byte[] data) {
return DigestUtils.sha256(DigestUtils.sha256(data));
}
/**
* 计算公钥的 RIPEMD160 Hash值
*
* @param pubKey 公钥
* @return ipeMD160Hash(sha256 ( pubkey))
*/
public static byte[] ripeMD160Hash(byte[] pubKey) {
//1. 先对公钥做 sha256 处理
byte[] shaHashedKey = DigestUtils.sha256(pubKey);
RIPEMD160Digest ripemd160 = new RIPEMD160Digest();
ripemd160.update(shaHashedKey, 0, shaHashedKey.length);
byte[] output = new byte[ripemd160.getDigestSize()];
ripemd160.doFinal(output, 0);
return output;
}
/**
* 生成公钥的校验码
*
* @param payload
* @return
*/
public static byte[] checksum(byte[] payload) {
return Arrays.copyOfRange(doubleHash(payload), 0, 4);
}
}
####
Wallet.java
新建wallet
包,并创建Wallet.java
文件。在Wallet.java
文件中编写代码如下:
package cldy.hanru.blockchain.wallet;
import cldy.hanru.blockchain.util.AddressUtils;
import cldy.hanru.blockchain.util.Base58Check;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECParameterSpec;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;
import java.security.Security;
/**
* @author hanru
*/
@AllArgsConstructor
@Data
public class Wallet implements Serializable {
private static final long serialVersionUID = 6796631943459965436L;
/**
* 校验码长度
*/
private static final int ADDRESS_CHECKSUM_LEN = 4;
/**
* 私钥
*/
private BCECPrivateKey privateKey;
/**
* 公钥
*/
private byte[] publicKey;
/**
* 初始化钱包
*/
private void initWallet() {
try {
KeyPair keyPair = newECKeyPair();
BCECPrivateKey privateKey = (BCECPrivateKey) keyPair.getPrivate();
BCECPublicKey publicKey = (BCECPublicKey) keyPair.getPublic();
byte[] publicKeyBytes = publicKey.getQ().getEncoded(false);
this.setPrivateKey(privateKey);
this.setPublicKey(publicKeyBytes);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 创建新的密钥对
*
* @return
* @throws Exception
*/
private KeyPair newECKeyPair() throws Exception {
// 注册 BC Provider
Security.addProvider(new BouncyCastleProvider());
// 创建椭圆曲线算法的密钥对生成器,算法为 ECDSA
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME);
// 椭圆曲线(EC)域参数设定
// bitcoin 为什么会选择 secp256k1,详见:https://bitcointalk.org/index.php?topic=151120.0
ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1");
keyPairGenerator.initialize(ecSpec, new SecureRandom());
return keyPairGenerator.generateKeyPair();
}
public Wallet() {
initWallet();
}
/**
* 获取钱包地址
*
* @return
*/
public String getAddress() {
try {
// 1. 获取 ripemdHashedKey
byte[] ripemdHashedKey = AddressUtils.ripeMD160Hash(this.getPublicKey());
// 2. 添加版本 0x00
ByteArrayOutputStream addrStream = new ByteArrayOutputStream();
addrStream.write((byte) 0);
addrStream.write(ripemdHashedKey);
byte[] versionedPayload = addrStream.toByteArray();
// 3. 计算校验码
byte[] checksum = AddressUtils.checksum(versionedPayload);
// 4. 得到 version + paylod + checksum 的组合
addrStream.write(checksum);
byte[] binaryAddress = addrStream.toByteArray();
// 5. 执行Base58转换处理
return Base58Check.rawBytesToBase58(binaryAddress);
} catch (IOException e) {
e.printStackTrace();
}
throw new RuntimeException("Fail to get wallet address ! ");
}
}
WalletUtils.java
文件打开cldy.hanru.blockchain.wallet
包,创建WalletUtils.java
文件。在WalletUtils.java
文件中编写代码如下:
添加代码如下:
package cldy.hanru.blockchain.wallet;
import cldy.hanru.blockchain.util.Base58Check;
import lombok.AllArgsConstructor;
import lombok.Cleanup;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.SealedObject;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.util.Map;
import java.util.Set;
/**
* 钱包工具类
*
* @author hanru
*/
public class WalletUtils {
/**
* 钱包文件
*/
private final static String WALLET_FILE = "wallet.dat";
/**
* 加密算法
*/
private static final String ALGORITHM = "AES";
/**
* 密文
*/
private static final byte[] CIPHER_TEXT = "2oF@5sC%DNf32y!TmiZi!tG9W5rLaniD".getBytes();
/**
* 定义内部类:钱包存储对象
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Wallets implements Serializable {
private static final long serialVersionUID = -4824448861236743729L;
private Map<String, Wallet> walletMap = new HashMap();
/**
* 添加钱包
*
* @param wallet
*/
private void addWallet(Wallet wallet) {
try {
this.walletMap.put(wallet.getAddress(), wallet);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取所有的钱包地址
*
* @return
* @throws Exception
*/
Set<String> getAddresses() throws Exception {
if (walletMap == null) {
throw new Exception("ERROR: Fail to get addresses ! There isn't address ! ");
}
return walletMap.keySet();
}
/**
* 获取钱包数据
*
* @param address 钱包地址
* @return
*/
Wallet getWallet(String address) throws Exception {
// 检查钱包地址是否合法
try {
Base58Check.base58ToBytes(address);
} catch (Exception e) {
throw new Exception("ERROR: invalid wallet address");
}
Wallet wallet = walletMap.get(address);
if (wallet == null) {
throw new Exception("ERROR: Fail to get wallet ! wallet don't exist ! ");
}
return wallet;
}
}
/**
* 钱包工具实例
*/
private volatile static WalletUtils instance;
public static WalletUtils getInstance() {
if (instance == null) {
synchronized (WalletUtils.class) {
if (instance == null) {
instance = new WalletUtils();
}
}
}
return instance;
}
private WalletUtils() {
initWalletFile();
}
/**
* 初始化钱包文件
*/
private void initWalletFile() {
File file = new File(WALLET_FILE);
if (!file.exists()) {
this.saveToDisk(new Wallets());
} else {
this.loadFromDisk();
}
}
/**
* 保存钱包数据
*/
private void saveToDisk(Wallets wallets) {
try {
if (wallets == null) {
throw new Exception("ERROR: Fail to save wallet to file ! data is null ! ");
}
SecretKeySpec sks = new SecretKeySpec(CIPHER_TEXT, ALGORITHM);
// Create cipher
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, sks);
SealedObject sealedObject = new SealedObject(wallets, cipher);
// Wrap the output stream
@Cleanup CipherOutputStream cos = new CipherOutputStream(
new BufferedOutputStream(new FileOutputStream(WALLET_FILE)), cipher);
@Cleanup ObjectOutputStream outputStream = new ObjectOutputStream(cos);
outputStream.writeObject(sealedObject);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 加载钱包数据
*/
private Wallets loadFromDisk() {
try {
SecretKeySpec sks = new SecretKeySpec(CIPHER_TEXT, ALGORITHM);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, sks);
@Cleanup CipherInputStream cipherInputStream = new CipherInputStream(
new BufferedInputStream(new FileInputStream(WALLET_FILE)), cipher);
@Cleanup ObjectInputStream inputStream = new ObjectInputStream(cipherInputStream);
SealedObject sealedObject = (SealedObject) inputStream.readObject();
return (Wallets) sealedObject.getObject(cipher);
} catch (Exception e) {
e.printStackTrace();
}
throw new RuntimeException("Fail to load wallet file from disk ! ");
}
/**
* 创建钱包
*
* @return
*/
public Wallet createWallet() {
Wallet wallet = new Wallet();
Wallets wallets = this.loadFromDisk();
wallets.addWallet(wallet);
this.saveToDisk(wallets);
return wallet;
}
/**
* 获取钱包数据
*
* @param address 钱包地址
* @return
*/
public Wallet getWallet(String address) throws Exception {
Wallets wallets = this.loadFromDisk();
return wallets.getWallet(address);
}
/**
* 获取所有的钱包地址
*
* @return
* @throws Exception
*/
public Set<String> getAddresses() throws Exception {
Wallets wallets = this.loadFromDisk();
return wallets.getAddresses();
}
}
CLI.java
文件打开cldy.hanru.blockchain.cli
包,修改CLI.java
文件。添加两个命令,创建钱包地址和打印钱包地址。
修改步骤:
修改步骤:
step1:修改help()方法,添加两个命令说明
step2:修改run()方法,增加两个分支
step3:新增两个方法用于创建钱包和打印钱包
createWallet()
printAddresses()
修改完后代码如下:
package cldy.hanru.blockchain.cli;
import cldy.hanru.blockchain.block.Block;
import cldy.hanru.blockchain.block.Blockchain;
import cldy.hanru.blockchain.pow.ProofOfWork;
import cldy.hanru.blockchain.store.RocksDBUtils;
import cldy.hanru.blockchain.transaction.TXInput;
import cldy.hanru.blockchain.transaction.TXOutput;
import cldy.hanru.blockchain.transaction.Transaction;
import cldy.hanru.blockchain.wallet.Wallet;
import cldy.hanru.blockchain.wallet.WalletUtils;
import org.apache.commons.cli.*;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import java.text.SimpleDateFormat;
import java.util.*;
public class CLI {
private String[] args;
private Options options = new Options();
public CLI(String[] args) {
this.args = args;
// options.addOption("h", "help", false, "show help");
// options.addOption("creategenesis", "creategenesis", false, "create blockchain with genesis block");
// options.addOption("add", "addblock", true, "add a block to the blockchain");
// options.addOption("print", "printchain", false, "print all the blocks of the blockchain");
Option helpCmd = Option.builder("h").desc("show help").build();
options.addOption(helpCmd);
Option address = Option.builder("address").hasArg(true).desc("Source wallet address").build();
Option sendFrom = Option.builder("from").hasArg(true).desc("Source wallet address").build();
Option sendTo = Option.builder("to").hasArg(true).desc("Destination wallet address").build();
Option sendAmount = Option.builder("amount").hasArg(true).desc("Amount to send").build();
options.addOption(address);
options.addOption(sendFrom);
options.addOption(sendTo);
options.addOption(sendAmount);
}
/**
* 打印帮助信息
*/
private void help() {
// HelpFormatter helpFormatter = new HelpFormatter();
// helpFormatter.printHelp("Main", options);
// System.exit(0);
System.out.println("Usage:");
System.out.println(" createwallet - Generates a new key-pair and saves it into the wallet file");
System.out.println(" printaddresses - print all wallet address");
System.out.println(" getbalance -address ADDRESS - Get balance of ADDRESS");
System.out.println(" createblockchain -address ADDRESS - Create a blockchain and send genesis block reward to ADDRESS");
System.out.println(" printchain - Print all the blocks of the blockchain");
System.out.println(" send -from FROM -to TO -amount AMOUNT - Send AMOUNT of coins from FROM address to TO");
System.exit(0);
}
/**
* 验证入参
*
* @param args
*/
private void validateArgs(String[] args) {
if (args == null || args.length < 1) {
help();
}
}
/**
* 命令行解析入口
*/
public void run() {
this.validateArgs(args);
CommandLineParser parser = new DefaultParser();
CommandLine cmd = null;
try {
cmd = parser.parse(options, args);
} catch (ParseException e) {
e.printStackTrace();
}
switch (args[0]) {
case "createblockchain":
String createblockchainAddress = cmd.getOptionValue("address");
if (StringUtils.isBlank(createblockchainAddress)) {
help();
}
this.createBlockchain(createblockchainAddress);
break;
case "getbalance":
String getBalanceAddress = cmd.getOptionValue("address");
if (StringUtils.isBlank(getBalanceAddress)) {
help();
}
try {
this.getBalance(getBalanceAddress);
} catch (Exception e) {
e.printStackTrace();
}finally {
RocksDBUtils.getInstance().closeDB();
}
break;
case "send":
String sendFrom = cmd.getOptionValue("from");
String sendTo = cmd.getOptionValue("to");
String sendAmount = cmd.getOptionValue("amount");
if (StringUtils.isBlank(sendFrom) ||
StringUtils.isBlank(sendTo) ||
!NumberUtils.isDigits(sendAmount)) {
help();
}
try {
this.send(sendFrom, sendTo, Integer.valueOf(sendAmount));
} catch (Exception e) {
e.printStackTrace();
}finally {
RocksDBUtils.getInstance().closeDB();
}
break;
case "printchain":
this.printChain();
break;
case "h":
this.help();
break;
case "createwallet":
try {
this.createWallet();
} catch (Exception e) {
e.printStackTrace();
}
break;
case "printaddresses":
try {
this.printAddresses();
} catch (Exception e) {
e.printStackTrace();
}
break;
default:
this.help();
}
}
/**
* 创建创世块
*/
// private void createBlockchainWithGenesisBlock () {
// Blockchain.newBlockchain();
// }
/**
* 创建区块链
*
* @param address
*/
private void createBlockchain(String address) {
Blockchain.createBlockchain(address);
System.out.println("Done ! ");
}
/**
* 添加区块
*
* @param data
*/
// private void addBlock (String data) throws Exception {
// Blockchain blockchain = Blockchain.newBlockchain();
// blockchain.addBlock(data);
// }
/**
* 打印出区块链中的所有区块
*/
private void printChain() {
// Blockchain blockchain = Blockchain.newBlockchain();
Blockchain blockchain = null;
try {
blockchain = Blockchain.initBlockchainFromDB();
} catch (Exception e) {
e.printStackTrace();
}
Blockchain.BlockchainIterator iterator = blockchain.getBlockchainIterator();
while (iterator.hashNext()) {
Block block = iterator.next();
System.out.println("第" + block.getHeight() + "个区块信息:");
if (block != null) {
boolean validate = ProofOfWork.newProofOfWork(block).validate();
System.out.println("validate = " + validate);
System.out.println("\tprevBlockHash: " + block.getPrevBlockHash());
// System.out.println("\tData: " + block.getData());
System.out.println("\tTransaction: ");
for (Transaction tx : block.getTransactions()) {
System.out.printf("\t\t交易ID:%s\n" , Hex.encodeHexString(tx.getTxId()));
System.out.println("\t\t输入:");
for (TXInput in : tx.getInputs()) {
System.out.printf("\t\t\tTxID:%s\n" , Hex.encodeHexString(in.getTxId()));
System.out.printf("\t\t\tOutputIndex:%d\n" , in.getTxOutputIndex());
System.out.printf("\t\t\tScriptSiq:%s\n" , in.getScriptSig());
}
System.out.println("\t\t输出:");
for (TXOutput out : tx.getOutputs()) {
System.out.printf("\t\t\tvalue:%d\n" , out.getValue());
System.out.printf("\t\t\tScriptPubKey:%s\n" , out.getScriptPubKey());
}
}
System.out.println("\tHash: " + block.getHash());
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date = sdf.format(new Date(block.getTimeStamp() * 1000L));
System.out.println("\ttimeStamp:" + date);
System.out.println();
}
}
}
/**
* 查询钱包余额
*
* @param address 钱包地址
*/
private void getBalance(String address) throws Exception {
Blockchain blockchain = Blockchain.createBlockchain(address);
TXOutput[] txOutputs = blockchain.findUTXO(address);
int balance = 0;
if (txOutputs != null && txOutputs.length > 0) {
for (TXOutput txOutput : txOutputs) {
balance += txOutput.getValue();
}
}
System.out.printf("Balance of '%s': %d\n", address, balance);
}
/**
* 转账
*
* @param from
* @param to
* @param amount
*/
private void send(String from, String to, int amount) throws Exception {
Blockchain blockchain = Blockchain.createBlockchain(from);
Transaction transaction = Transaction.newUTXOTransaction(from, to, amount, blockchain);
List<Transaction> transactions = new ArrayList<>();
transactions.add(transaction);
blockchain.mineBlock(transactions);
RocksDBUtils.getInstance().closeDB();
System.out.println("Success!");
}
/**
* 创建钱包
*
* @throws Exception
*/
private void createWallet() throws Exception {
Wallet wallet = WalletUtils.getInstance().createWallet();
System.out.println("wallet address : " + wallet.getAddress());
}
/**
* 打印钱包地址
*
* @throws Exception
*/
private void printAddresses() throws Exception {
Set<String> addresses = WalletUtils.getInstance().getAddresses();
if (addresses == null || addresses.isEmpty()) {
System.out.println("There isn't address");
return;
}
for (String address : addresses) {
System.out.println("Wallet address: " + address);
}
}
}
blockchain.sh
脚本文件最后修改blockchain.sh
脚本文件,修改后内容如下:
#!/bin/bash
set -e
# Check if the jar has been built.
if [ ! -e target/part6_Wallet-jar-with-dependencies.jar ]; then
echo "Compiling blockchain project to a JAR"
mvn package -DskipTests
fi
java -jar target/part6_Wallet-jar-with-dependencies.jar "$@"
##
这就是一个真实的比特币地址:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa。这是史上第一个比特币地址,据说属于中本聪。比特币地址是完全公开的,如果你想要给某个人发送币,只需要知道他的地址就可以了。但是,地址(尽管地址也是独一无二的)并不是用来证明你是一个“钱包”所有者的信物。实际上,所谓的地址,只不过是将公钥表示成人类可读的形式而已,因为原生的公钥人类很难阅读。在比特币中,你的身份(identity)就是一对(或者多对)保存在你的电脑(或者你能够获取到的地方)上的公钥(public key)和私钥(private key)。比特币基于一些加密算法的组合来创建这些密钥,并且保证了在这个世界上没有其他人能够取走你的币,除非拿到你的密钥。下图是创建钱包后到产生地址的流程图。
1、对于比特币来说,钱不是支付给个人的,而是支付给某一把私钥。这就是交易匿名性的根本原因,因为没有人知道,那些私钥背后的主人是谁。所以,比特币交易的第一件事,就是你必须拥有自己的公钥和私钥。
2、去网上那些比特币交易所开户,它们会让你首先生成一个比特币钱包(wallet)。这个钱包不是用来存放比特币,而是存放你的公钥和私钥。软件会帮你生成这两把钥匙,然后放在钱包里面。根据协议,公钥的长度是512位。这个长度不太方便传播,因此协议又规定,要为公钥生成一个160位的指纹。所谓指纹,就是一个比较短的、易于传播的哈希值。160位是二进制,写成十六进制,大约是26到35个字符,比如 1Ko29e6QcUXzhrtHD5aizMa6ustL88RTWR。这个字符串就叫做钱包的地址,它是唯一的,即每个钱包的地址肯定都是不一样的。
3、你向别人收钱时,只要告诉对方你的钱包地址即可,对方向这个地址付款。由于你是这个地址的拥有者,所以你会收到这笔钱。由于你是否拥有某个钱包地址,是由私钥证明的,所以一定要保护好私钥。同样的,你向他人支付比特币,千万不能写错他人的钱包地址,否则你的比特币就支付到了另一个不同的人了。
下面,让我们来讨论一下这些算法到底是什么。
公钥加密(public-key cryptography)算法使用的是成对的密钥:公钥和私钥。公钥并不是敏感信息,可以告诉其他人。但是,私钥绝对不能告诉其他人:只有所有者(owner)才能知道私钥,能够识别,鉴定和证明所有者身份的就是私钥。在加密货币的世界中,你的私钥代表的就是你,私钥就是一切。
本质上,比特币钱包也只不过是这样的密钥对而已。当你安装一个钱包应用,或是使用一个比特币客户端来生成一个新地址时,它就会为你生成一对密钥。在比特币中,谁拥有了私钥,谁就可以控制所有发送到这个公钥的币。
私钥和公钥只不过是随机的字节序列,因此它们无法在屏幕上打印,人类也无法通过肉眼去读取。这就是为什么比特币使用了一个转换算法,将公钥转化为一个人类可读的字符串(也就是我们看到的地址)。
如果你用过比特币钱包应用,很可能它会为你生成一个助记符。这样的助记符可以用来替代私钥,并且可以被用于生成私钥。BIP-039 已经实现了这个机制。
正如之前提到的,公钥和私钥是随机的字节序列。私钥能够用于证明持币人的身份,需要有一个条件:随机算法必须生成真正随机的字节。因为没有人会想要生成一个私钥,而这个私钥意外地也被别人所有。
比特币使用椭圆曲线来产生私钥。椭圆曲线是一个复杂的数学概念,我们并不打算在这里作太多解释(如果你真的十分好奇,可以查看这篇文章,注意:有很多数学公式!)我们只要知道这些曲线可以生成非常大的随机数就够了。在比特币中使用的曲线可以随机选取在 0 与 2 ^ 2 ^ 56(大概是 10^77, 而整个可见的宇宙中,原子数在 10^78 到 10^82 之间) 的一个数。有如此高的一个上限,意味着几乎不可能发生有两次生成同一个私钥的事情。
椭圆曲线算法(ECC,Elliptic Curve Cryptography)
比特币使用的是 ECDSA(Elliptic Curve Digital Signature Algorithm)算法来对交易进行签名,我们也会使用该算法。
比特币钱包随机生成私钥的安全性
每个用户的私钥是由比特币钱包随机生成的。可能有人会有疑问,这样随机生成的私钥安不安全,会不会我随机生成私钥跟别人的私钥恰好重复了?这个你不用过于担心,因为,比特币私钥是一串很长的文本,理论上比特币私钥的总数是:1461501637330902918203684832716283019655932542976
这是个什么概念?地球上的沙子是7.5乘以10的18次方,然后你想象一下,每粒沙子又是一个地球,这时的沙子总数是56.25乘以10的36次方,仍然远小于比特币私钥的总数。所以在这样一个庞大的地址空间下,私钥重复的概率微乎其微。
Base64就是一种基于64个可打印字符来表示二进制数据的方法
Base64使用了26个小写字母、26个大写字母、10个数字以及两个符号(例如“+”和“/”),用于在电子邮件这样的基于文本的媒介中传输二进制数据。
Base64通常用于编码邮件中的附件。
Base64字符集:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
Base58是一种基于文本的二进制编码格式,是用于Bitcoin中使用的一种独特的编码方式,主要用于产生Bitcoin的钱包地址。
Base58字符集:
ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz123456789
Base58Check
Base58Check是一种常用在比特币中的Base58编码格式,增加了错误校验码来检查数据在转录中出现的错误。在Base58Check中,对数据添加了一个称作“版本字节”的前缀,这个前缀用来明确需要编码的数据的类型。
Base58Check的作用
回到上面提到的比特币地址:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa 。现在,我们已经知道了这是公钥用人类可读的形式表示而已。如果我们对它进行解码,就会看到公钥的本来面目(16 进制表示的字节):0062E907B15CBF27D5425399EBF6F0FB50EBB88F18C29B7D93
比特币使用 Base58 算法将公钥转换成人类可读的形式。这个算法跟著名的 Base64 很类似,区别在于它使用了更短的字母表:为了避免一些利用字母相似性的攻击,从字母表中移除了一些字母。也就是,没有这些符号:0(零),O(大写的 o),I(大写的i),l(小写的 L),因为这几个字母看着很像。另外,也没有 + 和 / 符号。
比特币地址生成过程中的Base58Check的步骤如下:
1. 获取到公钥
2. 计算公钥的 SHA-256 哈希值(对公钥进行第一次hash256运算)
3. 计算 RIPEMD-160 哈希值(对第一次hash256的结果进行ripeMD160运算)
4. 添加版本前缀(将ripeMD160运算的结果前增加版本编号)
5. 计算两次hash(对加上版本编号的hash值计算两次hash)
6. 获取校验码(获取两次hash后前四个字节,作为校验码)
7. 形成比特币地址的16进制格式(将校验码作为比特币地址的后缀,与添加版本前缀的hash值拼接)
8. 进行Base58编码处理(对比特币地址的16进制格式进行Base58编码,就形成了比特币地址)
因此,为了使用Base58Check编码格式对数据(数字)进行编码,首先我们要对数据添加一个称作“版本字节”的前缀,这个前缀用来明确需要编码的数据的类型。
例如,比特币地址的前缀是0(十六进制是0x00),而对私钥编码时前缀是128(十六进制是0x80)。
所以公钥解码后包含三个部分:
Version Public key hash Checksum
00 62E907B15CBF27D5425399EBF6F0FB50EBB88F18 C29B7D93
由于哈希函数是单向的(也就说无法逆转回去),所以不可能从一个哈希中提取公钥。不过通过执行哈希函数并进行哈希比较,我们可以检查一个公钥是否被用于哈希的生成。
好了,所有细节都已就绪,来写代码吧。很多概念只有当写代码的时候,才能理解地更透彻。
由于Base58是在货币中特有的,并不是像Base64那样是通用编码方式,所以golang自带的包中没有实现,(Base64是有的,在"encoding/base64"包下),那么就需要我们自己写工具方法实现编码和解码。
在util
包下,新建一个java文件,命名为Base58Check.java
,并添加代码如下:
package cldy.hanru.blockchain.util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.util.Arrays;
/**
* Base58 转化工具
*
* @author hanru
*
*/
public final class Base58Check {
/*---- Class constants ----*/
private static final String ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
private static final BigInteger ALPHABET_SIZE = BigInteger.valueOf(ALPHABET.length());
/**
* 添加校验码并转化为 Base58 字符串
*
* @param data
* @return
*/
public static String bytesToBase58(byte[] data) {
return rawBytesToBase58(addCheckHash(data));
}
/**
* 转化为 Base58 字符串
*
* @param data
* @return
*/
public static String rawBytesToBase58(byte[] data) {
// Convert to base-58 string
StringBuilder sb = new StringBuilder();
BigInteger num = new BigInteger(1, data);
while (num.signum() != 0) {
BigInteger[] quotrem = num.divideAndRemainder(ALPHABET_SIZE);
sb.append(ALPHABET.charAt(quotrem[1].intValue()));
num = quotrem[0];
}
// Add '1' characters for leading 0-value bytes
for (int i = 0; i < data.length && data[i] == 0; i++) {
sb.append(ALPHABET.charAt(0));
}
return sb.reverse().toString();
}
/**
* 添加校验码并返回待有校验码的原生数据
*
* @param data
* @return
*/
static byte[] addCheckHash(byte[] data) {
try {
byte[] hash = Arrays.copyOf(AddressUtils.doubleHash(data), 4);
ByteArrayOutputStream buf = new ByteArrayOutputStream();
buf.write(data);
buf.write(hash);
return buf.toByteArray();
} catch (IOException e) {
throw new AssertionError(e);
}
}
/**
* 将 Base58Check 字符串转化为 byte 数组,并校验其校验码
* 返回的byte数组带有版本号,但不带有校验码
*
* @param s
* @return
*/
public static byte[] base58ToBytes(String s) {
byte[] concat = base58ToRawBytes(s);
byte[] data = Arrays.copyOf(concat, concat.length - 4);
byte[] hash = Arrays.copyOfRange(concat, concat.length - 4, concat.length);
byte[] rehash = Arrays.copyOf(AddressUtils.doubleHash(data), 4);
if (!Arrays.equals(rehash, hash)) {
throw new IllegalArgumentException("Checksum mismatch");
}
return data;
}
/**
* 将 Base58Check 字符串反转为 byte 数组
*
* @param s
* @return
*/
static byte[] base58ToRawBytes(String s) {
// Parse base-58 string
BigInteger num = BigInteger.ZERO;
for (int i = 0; i < s.length(); i++) {
num = num.multiply(ALPHABET_SIZE);
int digit = ALPHABET.indexOf(s.charAt(i));
if (digit == -1) {
throw new IllegalArgumentException("Invalid character for Base58Check");
}
num = num.add(BigInteger.valueOf(digit));
}
// Strip possible leading zero due to mandatory sign bit
byte[] b = num.toByteArray();
if (b[0] == 0) {
b = Arrays.copyOfRange(b, 1, b.length);
}
try {
// Convert leading '1' characters to leading 0-value bytes
ByteArrayOutputStream buf = new ByteArrayOutputStream();
for (int i = 0; i < s.length() && s.charAt(i) == ALPHABET.charAt(0); i++) {
buf.write(0);
}
buf.write(b);
return buf.toByteArray();
} catch (IOException e) {
throw new AssertionError(e);
}
}
/*---- Miscellaneous ----*/
private Base58Check() {
} // Not instantiable
}
在这个工具类中,我们提供了几个方法,逐一说明一下:bytesToBase58(),用于将字节数组,用Base58编码转为字符串。base58ToBytes(),用于将地址再转为字节数组,并对比校验码,进行校验地址是否有效。验证原理就是,将地址进行Base58解码,得到数据:版本号+hash数据+checksum,将版本号+hash数据,重新生成新的checksum,和原来的checksum进行比较,如果不同,说明地址无效,抛出异常打断程序执行。
还有一个点需要单独强调一下,在rawBytesToBase58()方法中:
// Add '1' characters for leading 0-value bytes
for (int i = 0; i < data.length && data[i] == 0; i++) {
sb.append(ALPHABET.charAt(0));
}
对于Base58编码,最后总会拼接下标0对应的Base58字符,就是1。所以我们所产生的地址的第一位,都是1。比如:1NH3bAuMAyXHnCrBmykPcKciBG6W3Dc5vY。
我们先从钱包 Wallet
结构开始:
在cldy.hanru.blockchain.wallet
包下,新建Wallet.java
文件,并添加Wallet类。
public class Wallet implements Serializable {
private static final long serialVersionUID = 6796631943459965436L;
/**
* 校验码长度
*/
private static final int ADDRESS_CHECKSUM_LEN = 4;
/**
* 私钥
*/
private BCECPrivateKey privateKey;
/**
* 公钥
*/
private byte[] publicKey;
/**
* 初始化钱包
*/
private void initWallet() {
try {
KeyPair keyPair = newECKeyPair();
BCECPrivateKey privateKey = (BCECPrivateKey) keyPair.getPrivate();
BCECPublicKey publicKey = (BCECPublicKey) keyPair.getPublic();
byte[] publicKeyBytes = publicKey.getQ().getEncoded(false);
this.setPrivateKey(privateKey);
this.setPublicKey(publicKeyBytes);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 创建新的密钥对
*
* @return
* @throws Exception
*/
private KeyPair newECKeyPair() throws Exception {
// 注册 BC Provider
Security.addProvider(new BouncyCastleProvider());
// 创建椭圆曲线算法的密钥对生成器,算法为 ECDSA
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("ECDSA", BouncyCastleProvider.PROVIDER_NAME);
// 椭圆曲线(EC)域参数设定
// bitcoin 为什么会选择 secp256k1,详见:https://bitcointalk.org/index.php?topic=151120.0
ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1");
keyPairGenerator.initialize(ecSpec, new SecureRandom());
return keyPairGenerator.generateKeyPair();
}
public Wallet() {
initWallet();
}
Wallet
的构造函数会生成一个新的密钥对。newKeyPair
函数非常直观:ECDSA 基于椭圆曲线,所以我们需要一个椭圆曲线。接下来,使用椭圆生成一个私钥,然后再从私钥生成一个公钥。有一点需要注意:在基于椭圆曲线的算法中,公钥是曲线上的点。因此,公钥是 X,Y 坐标的组合。在比特币中,这些坐标会被连接起来,然后形成一个公钥。
现在,来生成一个地址:
/**
* 获取钱包地址
*
* @return
*/
public String getAddress() {
try {
// 1. 获取 ripemdHashedKey
byte[] ripemdHashedKey = AddressUtils.ripeMD160Hash(this.getPublicKey());
// 2. 添加版本 0x00
ByteArrayOutputStream addrStream = new ByteArrayOutputStream();
addrStream.write((byte) 0);
addrStream.write(ripemdHashedKey);
byte[] versionedPayload = addrStream.toByteArray();
// 3. 计算校验码
byte[] checksum = AddressUtils.checksum(versionedPayload);
// 4. 得到 version + paylod + checksum 的组合
addrStream.write(checksum);
byte[] binaryAddress = addrStream.toByteArray();
// 5. 执行Base58转换处理
return Base58Check.rawBytesToBase58(binaryAddress);
} catch (IOException e) {
e.printStackTrace();
}
throw new RuntimeException("Fail to get wallet address ! ");
}
至此,就可以得到一个真实的比特币地址,你甚至可以在 blockchain.info 查看它的余额。不过我可以负责任地说,无论生成一个新的地址多少次,检查它的余额都是 0。这就是为什么选择一个合适的公钥加密算法是如此重要:考虑到私钥是随机数,生成同一个数字的概率必须是尽可能地低。理想情况下,必须是低到“永远”不会重复。
一个钱包对象,存储了一对私钥和公钥,公钥生成一个地址。现在我们可以创建一个钱包集合,可以存储多个地址。
我们需要 Wallets
类型来保存多个钱包的组合,将它们保存到文件中,或者从文件中进行加载。因为目前的钱包地址创建后,程序结束后内存就销毁了,那么我们需要将创建好的钱包地址,存储到本地文件中,进行持久化保存。
public static class Wallets implements Serializable {
private static final long serialVersionUID = -4824448861236743729L;
private Map<String, Wallet> walletMap = Maps.newHashMap();
/**
* 添加钱包
*
* @param wallet
*/
private void addWallet(Wallet wallet) {
try {
this.walletMap.put(wallet.getAddress(), wallet);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取所有的钱包地址
*
* @return
* @throws Exception
*/
Set<String> getAddresses() throws Exception {
if (walletMap == null) {
throw new Exception("ERROR: Fail to get addresses ! There isn't address ! ");
}
return walletMap.keySet();
}
/**
* 获取钱包数据
*
* @param address 钱包地址
* @return
*/
Wallet getWallet(String address) throws Exception {
// 检查钱包地址是否合法
try {
Base58Check.base58ToBytes(address);
} catch (Exception e) {
throw new Exception("ERROR: invalid wallet address");
}
Wallet wallet = walletMap.get(address);
if (wallet == null) {
throw new Exception("ERROR: Fail to get wallet ! wallet don't exist ! ");
}
return wallet;
}
}
如上,我们在WalletUtils.java中添加一个内部类:Wallets,用于表示钱包的集合,采用Map实现。对于map集合,一个钱包地址,对应了一个钱包对象。
/**
* 初始化钱包文件
*/
private void initWalletFile() {
File file = new File(WALLET_FILE);
if (!file.exists()) {
this.saveToDisk(new Wallets());
} else {
this.loadFromDisk();
}
}
然后,我们需要先定义一个文件:Wallets.dat
,用于存储钱包集合中的数据。然后创建了一个initWalletFile()
方法,用于初始化钱包文件。我们首先判断钱包文件是否存在,如果不存在,我们就直接创建一个空的Wallets钱包集合对象,存入钱包文件。
/**
* 创建钱包
*
* @return
*/
public Wallet createWallet() {
Wallet wallet = new Wallet();
Wallets wallets = this.loadFromDisk();
wallets.addWallet(wallet);
this.saveToDisk(wallets);
return wallet;
}
再创建获取一个createWallet()方法,用于创建钱包对象,首先加载本地文件,然后将创建好的钱包文件加入到钱包集合,重新持久化存储。
/**
* 保存钱包数据
*/
private void saveToDisk(Wallets wallets) {
try {
if (wallets == null) {
throw new Exception("ERROR: Fail to save wallet to file ! data is null ! ");
}
SecretKeySpec sks = new SecretKeySpec(CIPHER_TEXT, ALGORITHM);
// Create cipher
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, sks);
SealedObject sealedObject = new SealedObject(wallets, cipher);
// Wrap the output stream
@Cleanup CipherOutputStream cos = new CipherOutputStream(
new BufferedOutputStream(new FileOutputStream(WALLET_FILE)), cipher);
@Cleanup ObjectOutputStream outputStream = new ObjectOutputStream(cos);
outputStream.writeObject(sealedObject);
} catch (Exception e) {
e.printStackTrace();
}
}
接下来,在WalletUtils.java类中,添加方法,用于将钱包数据进行持久化存储,表示将钱包的数据保存到本地文件中。
/**
* 获取钱包数据
*
* @param address 钱包地址
* @return
*/
public Wallet getWallet(String address) throws Exception {
Wallets wallets = this.loadFromDisk();
return wallets.getWallet(address);
}
/**
* 获取所有的钱包地址
*
* @return
* @throws Exception
*/
public Set<String> getAddresses() throws Exception {
Wallets wallets = this.loadFromDisk();
return wallets.getAddresses();
}
现在我们又添加了两个方法,用于在获取钱包数据和获取所有的钱包地址方法中,调用加载的方法loadFromDisk():
/**
* 加载钱包数据
*/
private Wallets loadFromDisk() {
try {
SecretKeySpec sks = new SecretKeySpec(CIPHER_TEXT, ALGORITHM);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, sks);
@Cleanup CipherInputStream cipherInputStream = new CipherInputStream(
new BufferedInputStream(new FileInputStream(WALLET_FILE)), cipher);
@Cleanup ObjectInputStream inputStream = new ObjectInputStream(cipherInputStream);
SealedObject sealedObject = (SealedObject) inputStream.readObject();
return (Wallets) sealedObject.getObject(cipher);
} catch (Exception e) {
e.printStackTrace();
}
throw new RuntimeException("Fail to load wallet file from disk ! ");
}
接下来,在CLI.java
文件,修改Run(),添加一个分支,用于创建钱包:
func (cli *CLI) Run() {
//判断命令行参数的长度
this.validateArgs(args);
CommandLineParser parser = new DefaultParser();
CommandLine cmd = parser.parse(options, args);
switch (args[0]) {
...
case "createwallet":
this.createWallet();
break;
...
}
}
然后添加一个方法createWallet(),并添加代码如下:
/**
* 创建钱包
*
* @throws Exception
*/
private void createWallet() throws Exception {
Wallet wallet = WalletUtils.getInstance().createWallet();
System.out.println("wallet address : " + wallet.getAddress());
}
接下来,我们代码测试一下:
在终端中输入以下命令:
hanru:part6_Wallet ruby$ ./blockchain.sh createwallet
运行效果如下:
另外,注意:你并不需要连接到一个比特币节点来获得一个地址。地址生成算法使用的多种开源算法可以通过很多编程语言和库实现。
因为之前的代码已经包含了获取钱包地址的方法,所以只需要在CLI中添加一个命令即可。
我们在CLI.java
中,添加一个命令,用于打印出Wallets钱包集合中的所有的地址。
修改run()方法代码如下:
func (cli *CLI) Run() {
//判断命令行参数的长度
this.validateArgs(args);
CommandLineParser parser = new DefaultParser();
CommandLine cmd = parser.parse(options, args);
switch (args[0]) {
...
case "printaddresses":
try {
this.printAddresses();
} catch (Exception e) {
e.printStackTrace();
}
break;
...
}
}
接下来添加一个打印钱包地址的方法printAddresses(),代码如下:
/**
* 打印钱包地址
*
* @throws Exception
*/
private void printAddresses() throws Exception {
Set<String> addresses = WalletUtils.getInstance().getAddresses();
if (addresses == null || addresses.isEmpty()) {
System.out.println("There isn't address");
return;
}
for (String address : addresses) {
System.out.println("Wallet address: " + address);
}
}
最后,我们测试一下程序,在终端输入以下命令:
hanru:part6_Wallet ruby$ ./blockchain.sh printaddresses
运行效果如下:
通过本章节的学习,我们知道了如何创建一个钱包,钱包中如何创建一对秘钥。我们可以根据公钥生成钱包地址,这个过程虽然有点繁琐,但是不难理解。首先将公钥,进行一次sha256,一次ripemd160,进行hash散列,生成公钥hash(也叫指纹)。再用公钥hash前加1个byte的版本号,一般都是0x00,然后进行两次sha256,获取前4位,作为checksum,然后就得到了版本号+公钥hash+checksum的数据。最后进行一次Base58编码,就得到了钱包地址。
程序中的钱包地址都是存储在map中,为了进行持久化,我们还需要将创建好的钱包数据,保存到本地文件中。