Bitcoin私钥、公钥、地址概念以及Java实现


    这几个月一直在忙自己的终身大事,好久不写文章了;静下来分析写作中断的原因,发现一半是工作太忙,一半是自己太懒,坚持很多年做一件事情真得挺难的。从今天开始,我要拾起写作计划,写作可以梳理自己的知识框架,同时分享给大家一起学习,欢迎大家提出建议。

    之前的写作内容都是区块链入门级别的,出于个人爱好,主要围绕ethereum展开,但是工作中基本不涉及ethereum的内容,一直没有深入下去;由于工作围绕BitCoin展开,以后的文章我先转移到BTC领域。

    今天先从BTC的基本概念入手,整理一下私钥、公钥、地址的概念,这些概念在网络上已经泛滥很久了,实在是太枯燥,还是自己coding来得爽,今天就和大家分享如何用Java生成BTC私钥、公钥和地址。理论方面先分享几个链接:

    https://en.bitcoin.it/wiki/Private_key

    比特币密钥生成规则及 Go 实现 (特别推荐,这篇文章质量很高)

    这里强调一下理论的重要性,只有彻底理解了BTC私钥、公钥、地址的关系,才能coding出来,所有的代码只是思想的体现。

Java 代码实现

 代码部分只贴出了核心片段,稍微加工一下即可运行。

// 主要使用了java lang 有关椭圆曲线算法的package, bouncycastle lib(bcprov-jdk15on-160.jar)以及bitcoinj的Base58类

import java.security.KeyPair;

import java.security.KeyPairGenerator;

import java.security.spec.ECGenParameterSpec;

import java.security.spec.ECPoint;

import java.security.PublicKey;

import java.security.PrivateKey;

import java.security.interfaces.ECPrivateKey;

import java.security.interfaces.ECPublicKey;

import java.security.InvalidAlgorithmParameterException;

import java.security.NoSuchAlgorithmException;

import java.security.MessageDigest;

import java.security.NoSuchProviderException;

import java.security.Security;

import java.io.UnsupportedEncodingException;

import org.bouncycastle.jce.provider.BouncyCastleProvider;

import org.bitcoinj.core.Base58;

// 定义Key类,用来封装公私钥对和地址

class Key {

    private String privkey;

    private String pubkey;

    private String address;

    public Key() {

        Reset();

    }

    public Key(String privkey, String pubkey, String address) {

        this.privkey = privkey;

        this.pubkey  = pubkey;

        this.address = address;

    }

    public void Reset() {

        this.privkey = null;

        this.pubkey  = null;

        this.address = null;

    }

    public void SetPrivKey(String privkey) {

        this.privkey = privkey;

    }

    public void SetPubKey(String pubkey) {

        this.pubkey = pubkey;

    }

    public void SetAddress(String address) {

        this.address = address;

    }

    public String ToString() {

        return "{\n"

            + "\t privkey:" + this.privkey + "\n"

            + "\t pubkey :" + this.pubkey  + "\n"

            + "\t address:" + this.address + "\n"

            + "}\n";

    }

}

public class KeyGenerator {

   // Base58 encode prefix,不同的prefix可以定制地址的首字母

    static final byte PubKeyPrefix = 65;

    static final byte PrivKeyPrefix = -128;

    static final String PrivKeyPrefixStr = "80";

    static final byte PrivKeySuffix = 0x01;

    static int keyGeneratedCount = 1;

    static boolean debug = true;

    static KeyPairGenerator sKeyGen;

    static ECGenParameterSpec sEcSpec;

    static {

            Security.addProvider(new BouncyCastleProvider());

    }

    private static boolean ParseArguments(String []argv) {

        for (int i = 0; i < argv.length - 1; i++) {

            if ("-n".equals(argv[i])) {

                try {

                    keyGeneratedCount = Integer.parseInt(argv[i + 1]);

                    i = i + 1;

                    continue;

                } catch (NumberFormatException e) {

                    e.printStackTrace();

                    return false;

                }

            } else if ("-debug".equals(argv[i])) {

                debug = true;

            } else {

                System.out.println(argv[i] + " not supported...");

                return false;

            }

        }

        return keyGeneratedCount > 0;

    }

    public static void main(String args[]) {

        if (args.length > 1) {

          if (!ParseArguments(args)) {

              System.out.println("Arguments error, please check...");

              System.exit(-1);

          }

        }

        Key key = new Key();

        key.Reset();

        KeyGenerator generator = new KeyGenerator();

        for (int i = 0; i < keyGeneratedCount; i++) {

            key.Reset();

            if (generator.GenerateKey(key)) {

                System.out.println(key.ToString());

            } else {

                System.out.println("Generate key error...");

                System.exit(-1);

            }

        }

    }

    public KeyGenerator() {

        Init();

    }

    private void Init() {

        // Initialize key generator

        // The specific elliptic curve used is the secp256k1.

        try {

            sKeyGen = KeyPairGenerator.getInstance("EC");

            sEcSpec = new ECGenParameterSpec("secp256k1");

            if (sKeyGen == null) {

                System.out.println("Error: no ec algorithm");

                System.exit(-1);

            }

            sKeyGen.initialize(sEcSpec); // 采用secp256K1标准的椭圆曲线加密算法

        } catch (InvalidAlgorithmParameterException e) {

            System.out.println("Error:" + e);

            System.exit(-1);

        } catch (NoSuchAlgorithmException e) {

            System.out.println("Error:" + e);

            System.exit(-1);

        } catch (Exception e) {

            System.out.println("Error:" + e);

            System.exit(-1);

        }

    }

    public boolean GenerateKey(Key key) {

        key.Reset();

        // Generate key pair,依据椭圆曲线算法产生公私钥对

        KeyPair kp = sKeyGen.generateKeyPair();

        PublicKey pub = kp.getPublic();

        PrivateKey pvt = kp.getPrivate();

        ECPrivateKey epvt = (ECPrivateKey)pvt;

        String sepvt = Utils.AdjustTo64(epvt.getS().toString(16)).toUpperCase(); // 私钥16进制字符串

        if (debug) {

            System.out.println("Privkey[" + sepvt.length() + "]: " + sepvt);

        }

      // 获取X,Y坐标点,“04” + sx + sy即可获得完整的公钥,但是这里我们需要压缩的公钥

        ECPublicKey epub = (ECPublicKey)pub;

        ECPoint pt = epub.getW();

        String sx = Utils.AdjustTo64(pt.getAffineX().toString(16)).toUpperCase();

        String sy = Utils.AdjustTo64(pt.getAffineY().toString(16)).toUpperCase();

        String bcPub = "04" + sx + sy;

        if (debug) {

            System.out.println("Pubkey[" + bcPub.length() + "]: " + bcPub);

        }

        // Here we get compressed pubkey

       // 获取压缩公钥的方法:Y坐标最后一个字节是偶数,则 "02" + sx,否则 "03" + sx

        byte[] by = Utils.HexStringToByteArray(sy);

        byte lastByte = by[by.length - 1];

        String compressedPk;

        if ((int)(lastByte) % 2 == 0) {

            compressedPk = "02" + sx;

        } else {

            compressedPk = "03" + sx;

        }

        if (debug) {

            System.out.println("compressed pubkey: " + compressedPk);

        }

        key.SetPubKey(compressedPk);

        // We now need to perform a SHA-256 digest on the public key,

        // followed by a RIPEMD-160 digest.

       // 对压缩的公钥做SHA256摘要

        byte[] s1 = null;

        MessageDigest sha = null;

        try {

            sha = MessageDigest.getInstance("SHA-256");

            s1 = sha.digest(Utils.HexStringToByteArray(compressedPk));

            if (debug) {

                System.out.println("sha: " + Utils.BytesToHex(s1).toUpperCase());

            }

        } catch (NoSuchAlgorithmException e) {

            System.out.println("Error:" + e);

            return false;

        }

        // We use the Bouncy Castle provider for performing the RIPEMD-160 digest

        // since JCE does not implement this algorithm.

        // SHA256摘要之后做RIPEMD-160,这里调用Bouncy Castle的库,不知道的同学百度搜一下就懂了

        byte[] r1 = null;

        byte[] r2 = null;

        try {

            MessageDigest rmd = MessageDigest.getInstance("RipeMD160", "BC");

            if (rmd == null || s1 == null) {

                System.out.println("can't get ripemd160 or sha result is null");

                return false;

            }

            r1 = rmd.digest(s1);

            r2 = new byte[r1.length + 1];

            r2[0] = PubKeyPrefix; // RipeMD160 摘要之后加上公钥前缀

            for (int i = 0; i < r1.length; i++)

                r2[i + 1] = r1[i]; // 写的有点low,大家采用System.arraycopy自行修改吧

            if (debug) {

                System.out.println("rmd: " + Utils.BytesToHex(r2).toUpperCase());

            }

        } catch (NoSuchAlgorithmException e) {

            System.out.println("Error:" + e);

            return false;

        } catch (NoSuchProviderException e) {

            System.out.println("Error:" + e);

            return false;

        }

        byte[] s2 = null; // 加上前缀之后做两次SHA256

        if (sha != null && r2 != null) {

            sha.reset();

            s2 = sha.digest(r2);

            if (debug) {

                System.out.println("sha: " + Utils.BytesToHex(s2).toUpperCase());

            }

        } else {

            System.out.println("cant't do sha-256 after ripemd160");

            return false;

        }

        byte[] s3 = null;

        if (sha != null && s2 != null) {

            sha.reset();

            s3 = sha.digest(s2);

            if (debug) {

                System.out.println("sha: " + Utils.BytesToHex(s3).toUpperCase());

            }

        } else {

            System.out.println("cant't do sha-256 after sha-256");

            return false;

        }

        // 读懂下面内容,大家仔细阅读《比特币密钥生成规则及 Go 实现》

        byte[] a1 = new byte[r2.length + 4];

        for (int i = 0 ; i < r2.length ; i++) a1[i] = r2[i];

        for (int i = 0 ; i < 4 ; i++) a1[r2.length + i] = s3[i];

        if (debug) {

            System.out.println("before base58: " + Utils.BytesToHex(a1).toUpperCase());

        }

        key.SetAddress(Base58.encode(a1)); // 到此,可以获取WIF格式的地址

        if (debug) {

            System.out.println("addr: " + Base58.encode(a1));

        }

        // Lastly, we get compressed privkey 最后获取压缩的私钥

        byte[] pkBytes = null;

        pkBytes = Utils.HexStringToByteArray("80" + sepvt + "01");//sepvt.getBytes("UTF-8");

        if (debug) {

                System.out.println("raw compressed privkey: " + Utils.BytesToHex(pkBytes).toUpperCase());

            }

        try {

            sha = MessageDigest.getInstance("SHA-256");

        } catch (NoSuchAlgorithmException e) {

            System.out.println("Error:" + e);

            return false;

        }

        sha.reset();

        byte[] shafirst  = sha.digest(pkBytes);

        sha.reset();

        byte[] shasecond = sha.digest(shafirst);

        byte[] compressedPrivKey = new byte[pkBytes.length + 4];

        for (int i = 0; i < pkBytes.length; i++) {

            compressedPrivKey[i] = pkBytes[i];

        }

        for (int j = 0; j < 4; j++) {

            compressedPrivKey[j + pkBytes.length] = shasecond[j];

        }

        //compressedPrivKey[compressedPrivKey.length - 1] = PrivKeySuffix;

        key.SetPrivKey(Base58.encode(compressedPrivKey));

        if (debug) {

            System.out.println("compressed private key: " + Base58.encode(compressedPrivKey));

        }

        return true;

    }

}

// 附上Utils中的静态方法,都很简单

public class Utils {

    public static String AdjustTo64(String s) {

        switch(s.length()) {

            case 62: return "00" + s;

            case 63: return "0" + s;

            case 64: return s;

            default:

                throw new IllegalArgumentException("not a valid key: " + s);

        }

    }

    public static String BytesToHex(byte[] src) {

        StringBuilder stringBuilder = new StringBuilder("");

        if (src == null || src.length <= 0) {

            return null;

        }

        for (int i = 0; i < src.length; i++) {

            int v = src[i] & 0xFF;

            String hv = Integer.toHexString(v);

            if (hv.length() < 2) {

                stringBuilder.append(0);

            }

            stringBuilder.append(hv);

        }

        return stringBuilder.toString();

    }

    public static byte[] HexStringToByteArray(String s) {

        int len = s.length();

        byte[] data = new byte[len / 2];

        for (int i = 0; i < len; i += 2) {

            data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)

                + Character.digit(s.charAt(i+1), 16));

        }

        return data;

    }

}

总结

    如果以上代码仍有疑问,请仔细阅读上面推荐的两篇文章,请时刻记住:code只是理论的体现。

这里总结一下BitCoin 生成私钥、压缩格式私钥、公钥、压缩格式公钥、WIF钱包地址的过程:

a. secp256K1标准的EC算法生成公私钥对:(privkey, pubkey);

b. privkey 生成压缩格式私钥:假设privkey1 = 私钥前缀0x80+privkey+私钥后缀x01,

    result1 = sha256(sha256(privkey1)),对 privkey1做两次SHA256摘要, result1前4个字节添加到privkey1, privkey2 = privkey1 + result1[0:3],压缩私钥compPrivkey = base58(privkey2);

c. pubkey 生成完整的公钥和压缩格式的公钥:pubkey对应一个坐标点(X,Y),由X可以推算出Y,

    0x04 + X + Y就是完整的公钥;设Y的最后一个字节为b,则:

    b为偶数,压缩格式的公钥compPubkey = 0x02 + x,

    b为奇数,压缩格式的公钥compPubkey = 0x03 + x。

d. 压缩的公钥compPubkey生成WIF格式的地址address:

    假设 r1 =  RIPEMD160(SHA256(compPubkey)),压缩公钥先做SHA256,在做RIPEMD160摘要;

    假设 r2 = PubkeyPrefix(这里为10进制65) + r1;

    假设 s3 = SHA256(SHA256(r2)),r2两次SHA256摘要,s3的前4个字节为s3[0:3];

    假设 a = r2 + s3[0:3],WIF address = base58(a)。

    以上就是简单的总结,比较繁琐,至于为什么这么做,那是bitcoin设计师设计的,请大家查看官方资料。

    最后留给大家一个问题:compressed privkey如何得到完整的私钥匙??别看补充内容,自己先想想!

补充(压缩私钥匙转为完整私钥):

神奇的事情是这样发生的,对compressed privkey做base58 decode,结果为38个字节,结构为:

1字节前缀(0x80) + 32 字节私钥 + 1字节后缀(0x01) + 4 字节(这四个字节就是上面result1头4个字节)。

神奇吧?So amazing! 最后送给小伙伴们压缩私钥转换为原始私钥的code:

public static String convertWIFPrivkeyIntoPrivkey(String wifPrivKey) throws AddressFormatException {

        if (wifPrivKey == null || "".equals(wifPrivKey)) {

            throw new AddressFormatException("Invalid WIF private key");

        }

        byte[] base58Decode = null;

        try {

            base58Decode = Base58.decode(wifPrivKey);

        } catch (AddressFormatException e) {

            throw e;

        }

        String decodeStr = Utils.bytesToHexString(base58Decode);

        if (decodeStr.length() != 76) {

            throw new AddressFormatException("Invalid WIF private key");

        }

        String version = decodeStr.substring(0, 2);

        String suffix = decodeStr.substring(66, 68);

        if (!"80".equals(version) || !"01".equals(suffix)) {

            throw new AddressFormatException("Invalid WIF private key");

        }

        String privKeyStr = decodeStr.substring(2, 66);

        return privKeyStr;

    }

你可能感兴趣的:(Bitcoin私钥、公钥、地址概念以及Java实现)