IPFS主网上线已经有一段时间了,目前来说虽然Fil的币价不是过于理想,但是我们还是得相信IPFS拥有的潜力。

网上关于FIL离线生成地址和交易签名(JAVA版本)这块儿资料相对较少,正好最近花时间研究了一下,顺便整理了一下,供大家参考。

当然整个过程中,我们主要也不是摸着石头过河,其主要也是借鉴和参考了官方的SDK以及第三方库,然后将其翻译为Java版本。

在讲到FIL地址之前,我们需要了解几个算法概念。

BLAKE2

BLAKE2的定位是目前安全系数最高的哈希函数。BLAKE2是基于BLAKE实现的,BLAKE是2008年被提交至SHA-3竞赛的一种哈希函数。 BLAKE2不仅仅只是一个简单的哈希函数而已!首先,BLAKE2有两大主要版本:BLAKE2b和BLAKE2s。BLAKE2b是BLAKE的64位版本,它可以生成最高512位的任意长度哈希。BLAKE2s是BLAKE的32位版本,它可以生成最高256位的任意长度哈希。 BLAKE2x是对BLAKE2的简单扩展,它可以生成任意长度的哈希值(长度不受限制)。

Filcoin主要使用的是blake2b 算法.

ECC

椭圆曲线加密算法,即:Elliptic Curve Cryptography,简称ECC,是基于椭圆曲线数学理论实现的一种非对称加密算法。相比RSA,ECC优势是可以使用更短的密钥,来实现与RSA相当或更高的安全。

Base32编码

Base32编码是使用32个可打印字符(字母A-Z和数字2-7)对任意字节数据进行编码的方案,编码后的字符串不用区分大小写并排除了容易混淆的字符,可以方便地由人类使用并由计算机处理。Base32将任意字符串按照字节进行切分,并将每个字节对应的二进制值(不足8比特高位补0)串联起来,按照5比特一组进行切分,并将每组二进制值转换成十进制来对应32个可打印字符中的一个。

FIL的地址主要是4种类型,分别是 ID, SECP256K1,Actor,BLS,本系列主要讲述的地址以及签名算法均为SECP256K1。

地址生成步骤:

1.随机生成256位私钥(BIP44协议)

privateKey = generatePrivateKey()
"0x81232f16fa8f8bc2d31096d2407d9e392c25f048861a0e0f640f4febb4f22996"

2.利用椭圆曲线加密算法生成公钥

publicKey=ECC.getPublicKeyFromPrivateKey(privateKey)
"0xeb15c814543f9c1e62f1e4e13e2b5fd2a4d224b58f8cfeb1e587378d46a96c06dfaf876bc176911e7613c3e8ffe1e928025e266918a935fc6bd921606fcca5b3"

3.将公钥前加入0x04值后,进行20位的blake2b计算

blake2Hash = blake2b("0x04eb15c814543f9c1e62f1e4e13e2b5fd2a4d224b58f8cfeb1e587378d46a96c06dfaf876bc176911e7613c3e8ffe1e928025e266918a935fc6bd921606fcca5b3", 20)
"0x50cedfe81c8cdcf03fede2d075db9025d7f09463"

4.将得到的blake2哈希值前添加0x01后,继续用blake2b算法计算4位校验和。

checksum = blake2b("0x0150cedfe81c8cdcf03fede2d075db9025d7f09463", 4)
"0x62c27772"(4位转16进制就是8个字符)

5.将20位公钥哈希值和4位校验和连接起来,并用遵照RFC4648标准的Base32编码格式进行编码。

sourceAddress = Base32Encode(blake2Hash ,checksum, 'RFC4648')
"kdhn72a4rtopap7n4lihlw4qexl7bfddmlbho4q"

6.将编码后的字符串根据地址属性,

  • 测试网(t),
  • 正式网(f),
  • 钱包地址(1)
  • 合约地址(2)

加上相应的前缀,得到最终地址:

t1kdhn72a4rtopap7n4lihlw4qexl7bfddmlbho4q

参考代码

bip39生成助记词的过程忽略。


 public String createAddress(List<String> mnemonic) {

        long creationTimeSeconds = System.currentTimeMillis() / 1000;
        DeterministicSeed seed = new DeterministicSeed(mnemonic,null, "",creationTimeSeconds);
        DeterministicKeyChain keyChain = DeterministicKeyChain.builder().seed(seed).build();

        // Filcoin path路径
        List<ChildNumber> keyPath = HDUtils.parsePath("M/44H/461H/0H/0/0");

        DeterministicKey parent =
                        keyChain.getKeyByPath(keyPath, true);

        Base64 base64 = new Base64();
        final String encodedText = base64.encodeToString(parent.getPrivKeyBytes());

        String xprv = parent.getPrivateKeyAsHex();

        byte[] privateByte = parent.getPrivKeyBytes();

        // ECC椭圆加密
        ECKey ecKey = ECKey.fromPrivate(privateByte);

        // 未压缩公钥
        String pulStr =
                        "0x04" + ecKey.getPubKeyPoint().getAffineXCoord().toString() + ecKey.getPubKeyPoint().getAffineYCoord().toString();

        byte[] bytes =
                        NumericUtil.hexToBytes(pulStr);
        // 20位blake2b计算             
        byte[] byte1 =  this.blake2bHash(bytes, 20);
        String hash = Hex.toHexString(byte1);
        // 4位校验和blake2b计算
        String str2 = "0x01" + hash;
        byte[] byte2 = this.blake2bHash(NumericUtil.hexToBytes(str2), 4);
        byte[] addressBytes = new byte[byte1.length + byte2.length];
        System.arraycopy(byte1, 0, addressBytes, 0, byte1.length);
        System.arraycopy(byte2, 0, addressBytes, byte1.length,byte2.length);

        // Base32
        String address = "f1" + Base32New.encode(addressBytes);
}

base32类(apache.common 的base32库貌似也可以)

import org.bitcoinj.core.Sha256Hash;

public class Base32New extends Object {
    private static final String base32Chars = "abcdefghijklmnopqrstuvwxyz234567";
    private static final int[] base32Lookup = {0xFF, 0xFF, 0x1A, 0x1B, 0x1C,
            0x1D, 0x1E, 0x1F, // '0', '1', '2', '3', '4', '5', '6', '7'
            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // '8', '9', ':',
            // ';', '<', '=',
            // '>', '?'
            0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, // '@', 'A', 'B',
            // 'C', 'D', 'E',
            // 'F', 'G'
            0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, // 'H', 'I', 'J',
            // 'K', 'L', 'M',
            // 'N', 'O'
            0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, // 'P', 'Q', 'R',
            // 'S', 'T', 'U',
            // 'V', 'W'
            0x17, 0x18, 0x19, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 'X', 'Y', 'Z',
            // '[', '', ']',
            // '^', '_'
            0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, // '`', 'a', 'b',
            // 'c', 'd', 'e',
            // 'f', 'g'
            0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, // 'h', 'i', 'j',
            // 'k', 'l', 'm',
            // 'n', 'o'
            0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, // 'p', 'q', 'r',
            // 's', 't', 'u',
            // 'v', 'w'
            0x17, 0x18, 0x19, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF // 'x', 'y', 'z',
            // '{', '|', '}',
            // '~', 'DEL'
    };

    static public String encode(int version, byte[] payload) {
        if (version < 0 || version > 255)
            throw new IllegalArgumentException("Version not in range.");

        // A stringified buffer is:
        // 1 byte version + data bytes + 4 bytes check code (a truncated hash)
        byte[] addressBytes = new byte[1 + payload.length + 4];
        addressBytes[0] = (byte) version;
        System.arraycopy(payload, 0, addressBytes, 1, payload.length);
        byte[] checksum = Sha256Hash.hashTwice(addressBytes, 0, payload.length + 1);
        System.arraycopy(checksum, 0, addressBytes, payload.length + 1, 4);
        return encode(addressBytes);
    }

    /**
     * Encodes byte array to base32 String.
     *
     * @param bytes Bytes to encode.
     * @return Encoded byte array <code>bytes</code> as a String.
     */
    static public String encode(final byte[] bytes) {
        int i = 0, index = 0, digit = 0;
        int currByte, nextByte;
        StringBuffer base32 = new StringBuffer((bytes.length + 7) * 8 / 5);
        while (i < bytes.length) {
            currByte = (bytes[i] >= 0) ? bytes[i] : (bytes[i] + 256); // unsign
            /* Is the current digit going to span a byte boundary? */
            if (index > 3) {
                if ((i + 1) < bytes.length) {
                    nextByte = (bytes[i + 1] >= 0) ? bytes[i + 1]
                            : (bytes[i + 1] + 256);
                } else {
                    nextByte = 0;
                }
                digit = currByte & (0xFF >> index);
                index = (index + 5) % 8;
                digit <<= index;
                digit |= nextByte >> (8 - index);
                i++;
            } else {
                digit = (currByte >> (8 - (index + 5))) & 0x1F;
                index = (index + 5) % 8;
                if (index == 0)
                    i++;
            }
            base32.append(base32Chars.charAt(digit));
        }
        return base32.toString();
    }

    /**
     * Decodes the given base32 String to a raw byte array.
     *
     * @param base32
     * @return Decoded <code>base32</code> String as a raw byte array.
     */
    static public byte[] decode(final String base32) {
        int i, index, lookup, offset, digit;
        byte[] bytes = new byte[base32.length() * 5 / 8];
        for (i = 0, index = 0, offset = 0; i < base32.length(); i++) {
            lookup = base32.charAt(i) - '0';
            /* Skip chars outside the lookup table */
            if (lookup < 0 || lookup >= base32Lookup.length) {
                continue;
            }
            digit = base32Lookup[lookup];
            /* If this digit is not in the table, ignore it */
            if (digit == 0xFF) {
                continue;
            }
            if (index <= 3) {
                index = (index + 5) % 8;
                if (index == 0) {
                    bytes[offset] |= digit;
                    offset++;
                    if (offset >= bytes.length)
                        break;
                } else {
                    bytes[offset] |= digit << (8 - index);
                }
            } else {
                index = (index + 5) % 8;
                bytes[offset] |= (digit >>> index);
                offset++;
                if (offset >= bytes.length) {
                    break;
                }
                bytes[offset] |= digit << (8 - index);
            }
        }
        return bytes;
    }

    private static String toHex(byte[] decoded) {
        StringBuffer sb = new StringBuffer();
        sb.append("{");
        for (int i = 0; i < decoded.length; i++) {
            int b = decoded[i];
            if (b < 0) {
                b += 256;
            }
            sb.append("0x" + (Integer.toHexString(b + 256)).substring(1) + ",");
        }
        sb.deleteCharAt(sb.length() - 1);
        sb.append("}");
        return sb.toString();
    }

}

NumericUtil类

import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.regex.Pattern;

public class NumericUtil {
    private final static SecureRandom SECURE_RANDOM = new SecureRandom();
    private static final String HEX_PREFIX = "0x";

    public static byte[] generateRandomBytes(int size) {
        byte[] bytes = new byte[size];
        SECURE_RANDOM.nextBytes(bytes);
        return bytes;
    }

    public static boolean isValidHex(String value) {
        if (value == null) {
            return false;
        }
        if (value.startsWith("0x") || value.startsWith("0X")) {
            value = value.substring(2, value.length());
        }

        if (value.length() == 0 || value.length() % 2 != 0) {
            return false;
        }

        String pattern = "[0-9a-fA-F]+";
        return Pattern.matches(pattern, value);
        // If TestRpc resolves the following issue, we can reinstate this code
        // https://github.com/ethereumjs/testrpc/issues/220
        // if (value.length() > 3 && value.charAt(2) == '0') {
        //    return false;
        // }
    }

    public static String cleanHexPrefix(String input) {
        if (hasHexPrefix(input)) {
            return input.substring(2);
        } else {
            return input;
        }
    }

    public static String prependHexPrefix(String input) {
        if (input.length() > 1 && !hasHexPrefix(input)) {
            return HEX_PREFIX + input;
        } else {
            return input;
        }
    }

    private static boolean hasHexPrefix(String input) {
        return input.length() > 1 && input.charAt(0) == '0' && input.charAt(1) == 'x';
    }

    public static BigInteger bytesToBigInteger(byte[] value, int offset, int length) {
        return bytesToBigInteger((Arrays.copyOfRange(value, offset, offset + length)));
    }

    public static BigInteger bytesToBigInteger(byte[] value) {
        return new BigInteger(1, value);
    }

    public static BigInteger hexToBigInteger(String hexValue) {
        String cleanValue = cleanHexPrefix(hexValue);
        return new BigInteger(cleanValue, 16);
    }

    public static String bigIntegerToHex(BigInteger value) {
        return value.toString(16);
    }

//    public static String bigIntegerToHexWithZeroPadded(BigInteger value, int size) {
//        String result = bigIntegerToHex(value);
//
//        int length = result.length();
//        if (length > size) {
//            throw new UnsupportedOperationException(
//                    "Value " + result + "is larger then length " + size);
//        } else if (value.signum() < 0) {
//            throw new UnsupportedOperationException("Value cannot be negative");
//        }
//
//        if (length < size) {
//            result = Strings.repeat("0", size - length) + result;
//        }
//        return result;
//    }

    public static byte[] bigIntegerToBytesWithZeroPadded(BigInteger value, int length) {
        byte[] result = new byte[length];
        byte[] bytes = value.toByteArray();

        int bytesLength;
        int srcOffset;
        if (bytes[0] == 0) {
            bytesLength = bytes.length - 1;
            srcOffset = 1;
        } else {
            bytesLength = bytes.length;
            srcOffset = 0;
        }

        if (bytesLength > length) {
            throw new RuntimeException("Input is too large to put in byte array of size " + length);
        }

        int destOffset = length - bytesLength;
        System.arraycopy(bytes, srcOffset, result, destOffset, bytesLength);
        return result;
    }

    public static byte[] hexToBytes(String input) {
        String cleanInput = cleanHexPrefix(input);

        int len = cleanInput.length();

        if (len == 0) {
            return new byte[]{};
        }

        byte[] data;
        int startIdx;
        if (len % 2 != 0) {
            data = new byte[(len / 2) + 1];
            data[0] = (byte) Character.digit(cleanInput.charAt(0), 16);
            startIdx = 1;
        } else {
            data = new byte[len / 2];
            startIdx = 0;
        }

        for (int i = startIdx; i < len; i += 2) {
            data[(i + 1) / 2] = (byte) ((Character.digit(cleanInput.charAt(i), 16) << 4)
                    + Character.digit(cleanInput.charAt(i + 1), 16));
        }
        return data;
    }

    public static byte[] hexToBytesLittleEndian(String input) {
        byte[] bytes = hexToBytes(input);
        if (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN) {
            return bytes;
        }
        int middle = bytes.length / 2;
        for (int i = 0; i < middle; i++) {
            byte b = bytes[i];
            bytes[i] = bytes[bytes.length - 1 - i];
            bytes[bytes.length - 1 - i] = b;
        }
        return bytes;
    }

    public static byte[] reverseBytes(byte[] bytes) {
        int middle = bytes.length / 2;
        for (int i = 0; i < middle; i++) {
            byte b = bytes[i];
            bytes[i] = bytes[bytes.length - 1 - i];
            bytes[bytes.length - 1 - i] = b;
        }
        return bytes;
    }

    public static String bytesToHex(byte[] input) {
        StringBuilder stringBuilder = new StringBuilder();
        if (input.length == 0) {
            return "";
        }

        for (byte anInput : input) {
            stringBuilder.append(String.format("%02x", anInput));
        }

        return stringBuilder.toString();
    }

    public static String beBigEndianHex(String hex) {
        if (ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN) {
            return hex;
        }

        return reverseHex(hex);
    }

    public static String beLittleEndianHex(String hex) {
        if (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN) {
            return hex;
        }

        return reverseHex(hex);
    }

    private static String reverseHex(String hex) {
        byte[] bytes = hexToBytes(hex);
        bytes = reverseBytes(bytes);
        return bytesToHex(bytes);
    }

    public static int bytesToInt(byte[] bytes) {
        return ByteBuffer.wrap(bytes).getInt();
    }

    public static byte[] intToBytes(int intValue) {

        byte[] intBytes = ByteBuffer.allocate(4).putInt(intValue).array();
        int zeroLen = 0;
        for (byte b : intBytes) {
            if (b != 0) {
                break;
            }
            zeroLen++;
        }
        if (zeroLen == 4) {
            zeroLen = 3;
        }
        return Arrays.copyOfRange(intBytes, zeroLen, intBytes.length);
    }
}