从Java角度看区块链实践系列4:基于原理手写实现SHA-256算法以及Merkle树算法

上一节课,我们讲到Merkle树的原理,其本质是一种哈希二叉树结构,叶子借贷你的value源于数据的哈希,非叶子节点的value则是根据它左右两个孩子节点进行双哈希计算得出。

Merkle树算法在区块链中被广泛使用,其应用面包括零知识证明、文件完整性校验等领域。

现在,让我们进入主题,实战手写SHA-256算法以及Merkle树。

加密算法

目前,加密算法主要分为对称加密算法、非对称加密算法以及不可逆加密算法三大类。

对称加密算法

对称加密算法是应用较早的加密算法,对称加密算法加密的解密的密钥相同,其密钥实现主要有分组密码和序列密码两种实现方式。对称加密算法的主要特点是算法公开、计算量小、加密速度快、效率高,但加解密使用同一密钥,安全性得不到保障。目前,主流对称加密算法有DES、3DES、IDEA以及美国国家标准局的AES。

非对称加密算法

由于加密和解密使用不同密钥,故称为“非对称加密”,其密钥分为公钥和私钥,公私钥成对配合使用。非对称加密算法可以实现数字签名。常见的有RSA算法、美国国家标准局与技术研究院(NIST)提出的DSA算法。特点是加密速度慢、效率低。(在签名部分会详细讲解RSA原理)

不可逆加密算法

不可逆加密算法加密不需要使用密钥,输入明文后由系统直接经过加密算法处理成密文,是一种单向摘要加密算法,加密后的算法难以被破解,校验则需要使用原有数据使用同种加密算法加密,对结果进行比较。目前,被广泛使用的主要是RSA公司研发的MD5以及美标局推荐的基于SHS(Secure Hash Standard,安全哈希标准)标准的算法等。

哈希散列函数

维基这样定义它:

散列函数是一种可以从任意大小的数据创建固定大小数据的函数。散列函数把消息或数据压缩成摘要,使得数据量变小,将数据的格式固定下来。该函数将数据打乱混合,重新创建一个叫做散列值(hash values,hash codes,hash sums,或hashes)的指纹。

主要特点是压缩性强、计算简单、加密结果单向性,通过哈希碰撞概率问题保证安全性。它将无限的内容压缩到有限位数的取值范围内,不可避免有效概率出现哈希碰撞的可能性,因此哈希函数必须具备良好的抗碰撞性。

目前,哈希算法主要分MD系列(MD4、MD5、HAVAL)和SHA系列(SHA-1、SHA-256),这里介绍SHA系列的SHA256。

1byte字节=8bit位,一个十六进制的字符的长度为4bit位。

SHA-1算法

SHA-1算法的输入长度限制在264位,输入消息按照512位一个组进行分组处理,输出结果是160位的消息摘要。SHA-1算法的速度高、实现简单。

SHA-2算法

SHA-2算法输出长度可取224位、256位、384位、512位,分别对应SHA-224、SHA-256、SAH-384、SHA-512,另外还有SHA-512/224、SHA-512/256,这两个算法比之前的算法具备更高的安全性和输出长度的灵活性。这些变体除了生成摘要的长度、循环运行的次数等一些细微差异之外,基本结构是一致的。

SHA-3算法

美标局选择Keccak算法作为SHA-3的加密标准,Keccak算法具有良好的加密性能以及抗解密能力。

SHA-256的数学基础

自然数:大于零的整数。

质数:指在大于1的自然数中,除了1和它本身意外不再有其他因数的自然数。

取模运算:又称“模除”,即求两个自然数做除法运算的余数。若有自然数 a、b,则 a % b = c 或 a mod b = c,当 a < b 时,c = a。关于模的运算比较多,除了简单的四则运算以外还有我们后续签名算法会用到的模逆元。

高位/低位:所谓高位/即是二进制数字,做位移运算时,左边被移除的bit位,低位则相反,为位移运算后不足的bit位。

二进制左移(<<):m<<n将整数m表示的二进制数左移n位,高位移出n位都舍弃,低位补0.(此时将会出现正数变成负数的可能)。m<<n即在数字没有溢出的前提下,对于正数和负数,左移n位都相当于m乘以2的n次方。

例如5左移4位(由于Java int类型占32bit位,所以这里按32位二进制演示):

5的二进制:0000 0000 0000 0000 0000 0000 0000 0101

向左移4位:0000 0000 0000 0000 0000 0000 0101 0000(不足补0,结果为十进制80)

二进制右移(>>):m>>n把整数m表示的二进制数右移n位,m为正数,高位全部补0;m为负数,高位全部补1。m>>n即相当于m除以2的n次方,得到的为整数时,即为结果。如果结果为小数,此时会出现两种情况:

若m为正数,得到的商会无条件 的舍弃小数位;

若m为负数,舍弃小数部分,然后把整数部分加+1得到位移后的值。

无符号右移( >>> ):m>>>n:整数m表示的二进制右移n位,不论正负数,高位都补0。

位与( & :逐个比较两个二进制字符串,若两个都为1才为1,其他情况均为0。例如 5 & 3:

5的二进制:0000 0000 0000 0000 0000 0000 0000 0101

3的二进制:0000 0000 0000 0000 0000 0000 0000 0011

位与结果:  0000 0000 0000 0000 0000 0000 0000 0001

位或( | :逐个比较两个二进制字符串,若两个都为0才为0,其他情况均为1。

位非( ~ :对单个二进制字符串,二进制位的内容取反。是1则变为0,0则变为1。

位异或( ^ :逐个比较两个二进制字符串,若位值相同,则为0,反之则为1。例如:

5的二进制:0000 0000 0000 0000 0000 0000 0000 0101

3的二进制:0000 0000 0000 0000 0000 0000 0000 0011

位异或后:  0000 0000 0000 0000 0000 0000 0000 0110

SHA-256涉及到的运算符与进制换算表

bit代表二进制位,byte代表字节,word是SHA256算法中的最小运算单元,称为“字”(Word)。

1 byte = 8 bit

1 int  = 1 word = 4 byte = 4*8 = 32 bit

公式逻辑运算符 Java逻辑运算符 含 义
& 按位“与”
¬ ~ 按位“补/非”
 | 按位“异/或”
S^{n} Integer.rotateRight() 循环右移n个bit
R^{n} >>> 无符号右移n个bit

涉及到的逻辑函数Java实现表:

逻辑函数 Java实现
\sigma _0 (x) = S^7(x) \bigoplus S^{18}(x) \bigoplus R^3(x)
    private static int smallSig0(int x) {
        return Integer.rotateRight(x, 7) ^ Integer.rotateRight(x, 18)
                ^ (x >>> 3);
    }
\sigma _1(x) = S^{17}(x) \bigoplus S^{19}(x) \bigoplus R^{10}(x)
private static int smallSig1(int x) {
    return Integer.rotateRight(x, 17) ^ Integer.rotateRight(x, 19)
            ^ (x >>> 10);
}
\large Ch(x,y,z) = (x\wedge y)\oplus ( \sim x\wedge z)
private static int ch(int x, int y, int z) {
   return (x & y) | ((~x) & z);
}
Ma(x,y,z) = (x\wedge y) \bigoplus (x\wedge z) \bigoplus (y\wedge z)
private static int maj(int x, int y, int z) {
    return (x & y) | (x & z) | (y & z);
}
\sum _0(x) = S^2(x) \bigoplus S^{13}(x) \bigoplus S^{22}(x)
private static int bigSig0(int x) {
    return Integer.rotateRight(x, 2) ^ Integer.rotateRight(x, 13)
            ^ Integer.rotateRight(x, 22);
}
\sum _1(x) = S^6(x) \bigoplus S^{11}(x) \bigoplus S^{25}(x)
private static int bigSig1(int x) {
    return Integer.rotateRight(x, 6) ^ Integer.rotateRight(x, 11)
            ^ Integer.rotateRight(x, 25);
}

SHA-256

SHA-256被广泛应用于比特币区块链,基本所有涉及摘要加密的地方比特币都选用了SHA-256,同时在公链项目中也随处可见SHA-256的应用。

由于雪崩效应,即使消息有很小的改变,也将导致最终Hash结果的不同。

对于任意长度的消息,SHA-256都会输出一个256bit位的Hash摘要,这个摘要通常是一个长度为64的十六进制字符串,相当于是4个个长度为32-byte字节的数组。SHA-256算法中最小运算单元称为“字”(Word),一个字是32位。

加密过程:

1、设置初始化常量;

2、对输入消息进行补位,补位后的长度为512bit位的倍数;

3、按照每个512bit位对补位后的消息进行分块:

                                                                     \LARGE M^2,M^2,.....,M^N

4、对每个块执行一系列复杂的逻辑运算,最终得到哈希摘要:

                                                             \LARGE H^{}i = H^{(i - 1)} + {{C_{M}}^{i}} (H^{(i - 1)})
接下来我们按照这个实现SHA-256算法。

SHA-256原理详解

常量初始化

1、初始化8个原始哈希H0,取自然数前8个素数/质数(2,3,5,7,11,13,17,19)的平方根的小数部分的前32bit位。

比如:\sqrt{2} \approx 0.414213562373095048\approx6*16-1+a*16-2+0*16-3+...

于是,质数2的平方根的小数部分取前32bit用16进制表示就为0x6a09e667。

    private static final int[] H0 = {0x6a09e667, 0xbb67ae85, 0x3c6ef372,
            0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19};

2、初始化64个常数密钥,取自自然数中前面64个素数的 立方根的小数部分的前32bit位,用16进制表示, 则相应的常数序列如下:

我们使用K_{t}表示第t个密钥。 

    private static final int[] K = {0x428a2f98, 0x71374491, 0xb5c0fbcf,
            0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
            0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74,
            0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
            0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc,
            0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
            0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85,
            0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb,
            0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70,
            0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
            0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3,
            0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f,
            0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
            0xc67178f2};

消息预处理——补位

补位操作遵循如下方程式:

                                                       \large l + 1 + k \equiv 448 mod 512

为什么是448?

因为在补位的第一步的预处理后,第二步会附加上一个64bit的数据,用来表示原始报文的长度信息。而448+64=512,正好拼成了一个完整的结构。

我们以SHA256(“abc”)为例:

原始字符 ASCII码 二进制格式
a 97 01100001
b 98 01100010
c 99 01100011

因此,其二进制形式为:01100001 01100010 01100011,长度\large l = 24

1、补1,即直接在二进制消息尾部添加 1;

例如:消息“abc”的二进制位,补1后 01100001 01100010 01100011 1。

2、补0,个数为 \large k,代入消息长度 \large l可求得;

例如:代入 \large l 算得 \large k = 448 - l -1 = 423,因此补 423个0。由于448 < 512,因此,可忽略掉后面的mod 512(对整数取模时,若整数小于模数,则取模结果为本身)。因此补位后,01100001 01100010 01100011 10000000……00000000。

3、补入消息长度 \large l 的64位二进制内容,若长度的二进制不足64位,则在长度前面添加0,知道二进制字符串长度为64。

例如:消息“abc”的二进制长度为24,则最后进行的补位操作就是将24的二进制形式 ‭00011000‬。

调用pad()方法可以获取到补位的结果,代码实现<单击一键,代码尽显>:

    /**
     * 填充给定消息的长度
     * 512位(64字节)的倍数,包括添加 1位,k 0位以及消息长度为64位整数。
     *
     * @param message 要填充的消息,String.getBytes()可获得消息的字节数组。
     * @return 一个带有填充消息字节的新数组。
     */
    public static byte[] pad(byte[] message) {
        final int blockBits = 512;
        final int blockBytes = blockBits / 8;

        // 新消息长度:原始长度 + (补位的)1位 + 填充8字节长度(也就是64bit)
        int newMessageLength = message.length + 1 + 8;
        int padBytes = blockBytes - (newMessageLength % blockBytes);
        newMessageLength += padBytes;

        // 将消息复制到扩展数组
        final byte[] paddedMessage = new byte[newMessageLength];
        System.arraycopy(message, 0, paddedMessage, 0, message.length);

        // 第一步,补位:在消息末尾补上一位"1"。0b代表二进制,10000000 是二进制的128
        paddedMessage[message.length] = (byte) 0b10000000;

        // 第二步,跳过,因为我们已经设置了padBytes数组的长度,所以内部所有元素已经是0了(默认值)

        // 第三步,补入消息长度l的64位二进制的8字节整数,(java使用的是byte,1 byte = 8 bit)
        int lenPos = message.length + 1 + padBytes;
        ByteBuffer.wrap(paddedMessage, lenPos, 8).putLong(message.length * 8);
        return paddedMessage;
    }

加密计算Hash摘要

1、将补位后的消息分解成n个块,每一个块512bit

用java实现时,我们不需要按块“存储”,因此可以省略此步。 

2、遍历所有区块,将单个区块从16 word重构成64 word。

对于每一块,将块分解为16个32-bit的big-endian的字,记为w[0], …, w[15]。当前区块的第t个“字”我们用W_{t}表示

2.1 前16个字直接由消息的第t个块分解得到经过第一步进行区块分组后,每个区块都包含了16 word。

    /**
     * 块数组
     */
    private static final int[] W = new int[64];

    public  static byte[] hash(byte[] message) {
        // ……省略部分代码
            System.arraycopy(words, i * 16, W, 0, 16);
        // ……省略部分代码
    }

2.2 其余的字由如下迭代公式得到

                                                  \large W_{t} = \sigma _{1}(W_{t - 2}) + W_{t-7}+ \sigma _{0}(W_{t - 15}) + W_{t - 16}

    /**
     * 块数组
     */
    private static final int[] W = new int[64];

    public  static byte[] hash(byte[] message) {
        // ……省略部分代码  smallSig1为 σ1​ 函数,smallSig0为 σ0 函数
            for (int t = 16; t < W.length; ++t) {
                // 根据加法交换律,修改先后顺序不印象最终结果
                W[t] = smallSig1(W[t - 2])
                        + W[t - 7]
                        + smallSig0(W[t - 15])
                        + W[t - 16];
            }
        // ……省略部分代码
    }

 3、循环对每个“块”加密,也就是说需要循环64次。

经过步骤二后,W将成为具备64 word,加密的过程如下图:

SAH-256算法函数调用图
SAH-256算法加密函数调用图

 

代码实现<单击一键,代码尽显>:

    /**
     * 块数组
     */
    private static final int[] W = new int[64];

    public  static byte[] hash(byte[] message) {
        // ……省略部分代码  


        /*
               3、循环对“块”加密:也就是说循环64次。
               例如:对“abc”加密时,就相当于依次对 c、b、a进行加密,c的加密结果,保存到a位置
             */

            // 设 TEMP = H,H是初始变量,即8个初始hash值,也就是前8个质数的平方根的前32bit位。
            System.arraycopy(H, 0, TEMP, 0, H.length);

            // 在TEMP上操作
            for (int t = 0; t < W.length; ++t) {
                // t1 = H[7] + Ch(H[4],H[5],H[6]) + Σ1(H[4])
                int t1 = TEMP[7]
                        + ch(TEMP[4], TEMP[5], TEMP[6]) + K[t] + W[t]
                        + bigSig1(TEMP[4]);

                // t2 = Ma(H[0],H[1],H[2]) + Σ0(H[0])
                int t2 = maj(TEMP[0], TEMP[1], TEMP[2]) + bigSig0(TEMP[0]);
                System.arraycopy(TEMP, 0, TEMP, 1, TEMP.length - 1);

                // 设置中间散列
                TEMP[4] += t1;

                // 设置头部散列 t1 + t2
                TEMP[0] = t1 + t2;
            }

            // 将TEMP中的值添加到H中的值
            for (int t = 0; t < H.length; ++t) {
                H[t] += TEMP[t];
            }
        // ……省略部分代码
    }

经过64轮后,代码中的H就是最终SHA-256加密的结果。

Java实现Merkle树

Merkle树原理在上一节,我们已经详细阐述过,这里就直接贴实现的代码了<复习快速跳转>。

定义叶子节点<单击一键,代码尽显>

@Data
public class TreeNode {
    /**
     * 左子节点
     */
    private TreeNode left;
    /**
     * 右子节点
     */
    private TreeNode right;
    /**
     * (孩子)节点数据
     */
    private String data;
    /**
     * SHA-256的data
     */
    private String hash;

    public TreeNode(){}
    public TreeNode(String data) {
        this.data = data;
        this.hash = DigestUtil.sha256Hex(data);
    }
}

Merkle树计算<单击一键,代码尽显>

package org.lmx.common.merkle;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.digest.DigestUtil;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

/**
 * 功能描述:Merkle算法实现
 *
 * @program: block-chain-j
 * @author: LM.X
 * @create: 2020-03-31 14:53
 **/
@Slf4j
public class MerkleTree {
    /**
     * 交易列表
     */
    private List<TreeNode> treeNodes;

    /**
     * 根节点
     */
    private TreeNode root;

    public MerkleTree(List<String> treeNodes) {
        createMerkleTree(treeNodes);
    }

    /**
     * 功能描述: 构建默克尔树
     *
     * @param transactions 内容列表
     * @return void
     * @author LM.X
     * @date 2020/3/31 14:58
     */
    private void createMerkleTree(List<String> transactions) {
        if (CollectionUtil.isEmpty(transactions)) {
            return;
        }

        // 初始化列表
        this.treeNodes = new ArrayList();

        // 格式化节点信息
        treeNodes.addAll(createLeafNode(transactions));

        // 合并叶子节点,获取默克尔根
        while (true) {
            treeNodes = createParentList(treeNodes);
            if (treeNodes.size() < 2) {
                root = treeNodes.get(0);
                return;
            }
        }
    }

    /**
     * 功能描述: 创建叶子节点
     *
     * @param transactions 内容列表
     * @return 返回叶子节点列表
     * @author LM.X
     * @date 2020/3/31 15:09
     */
    private List<TreeNode> createLeafNode(List<String> transactions) {
        List<TreeNode> leafs = new ArrayList();
        if (CollectionUtil.isEmpty(transactions)) {
            return leafs;
        }

        for (String transaction : transactions) {
            leafs.add(new TreeNode(transaction));
        }

        return leafs;
    }

    /**
     * 功能描述: 合并所以叶子节点
     *
     * @param nodes 节点列表
     * @return 返回合并后的节点集合
     * @author LM.X
     * @date 2020/3/31 15:41
     */
    private List<TreeNode> createParentList(List<TreeNode> nodes) {
        List parents = new ArrayList();
        if (CollectionUtil.isEmpty(nodes)) {
            return parents;
        }

        int len = nodes.size();

        for (int i = 0; i < len - 1; i += 2) {
            parents.add(createParentNode(nodes.get(i), nodes.get(i + 1)));
        }

        // 当奇数个叶子节点时,单独处理
        if (len % 2 != 0) {
            parents.add(createParentNode(nodes.get(len - 1), null));
        }

        log.info("本轮合并后,节点长度:{}", parents.size());
        return parents;
    }

    /**
     * 功能描述: 合并左右子节点
     *
     * @param left  左子节点
     * @param right 右子节点
     * @return 返回合并后的父节点
     * @author LM.X
     * @date 2020/3/31 15:35
     */
    private TreeNode createParentNode(TreeNode left, TreeNode right) {
        TreeNode parent = new TreeNode();
        parent.setLeft(left);
        parent.setRight(right);

        String lh = left.getHash();

        String hash = ObjectUtil.isEmpty(right) ? lh : doubleSHA256(lh, right.getHash());

        parent.setData(hash);
        parent.setHash(hash);
        log.info("合并【{},{}】,创建父节点:{}", left.getData(), ObjectUtil.isEmpty(right) ?
                null : right.getData(), hash);
        return parent;
    }

    /**
     * 功能描述: 双哈希运算
     *
     * @param lh
	 * @param rh
     * @return SHA256 结果
     * @author LM.X
     * @date 2020/3/31 16:02
     */
    private String doubleSHA256(String lh, String rh) {
        return DigestUtil.sha256Hex(DigestUtil.sha256Hex(lh + rh));
    }

    public static void main(String[] args) {
        List<String> txs = new ArrayList() {{
            add("1");add("2");add("3");add("4");
            add("5");add("6");add("7");add("8");
            add("9");add("10");add("11");add("12");
            add("13");add("14");add("15");add("16");
        }};

        MerkleTree merkleTree = new MerkleTree(txs);
        log.info("获取到的默克尔根为:{}", merkleTree.root.getHash());
    }
}

总结

这一节,我们剖析了在区块链的底层核心所涉及的加密算法,深入了解如今被广泛使用的SHA-256算法的实现原理,其通过简单的位移取模操作生成摘要,SHA256的抗碰撞性保证了信息的安全。

之后我们实战了Merkle树算法,从实战中可以了解更多它的优缺点,也可以检验上篇我们讲解的Merkle原理,进而让我们在了解区块链的道路上更近一步。

好啦,今天我们到这里就结束啦!

近期由于搬家找房子后续文章可能会有所延后发布,文章有不足之处,欢迎指出,我将不断完善之~

最后,给大家奉上后续Java实战计划安排表:

目录 内容
P2P网络实战 这一篇将为大家带来Java实现P2P通讯网络的实战。
区块链共识系列 学习共识系列你将会了解到目前最为完整的共识算法发展史,还能学习到Java实战实现共识算法。包括:RAFT共识、经典的PWD、PWD、DPOS共识、最新的PoET、POB共识、以及混合共识。
签名算法系列:RSA签名算法 这一篇你将学习到RSA的密码学、RSA签名原理及实战
签名算法系列:ECC椭圆曲线签名 学习ECC椭圆曲线加密算法你讲收获椭圆曲线密码学、椭圆曲线数学等相关知识。
区块链安全

讲解相关安全事故以及预防措施。

Solidity合约编写 讲解合约实现原理、实战合约。包含ERC20、ERC-223、ERC-721、NEP系列、TRC系列等合约编写等实战。
合约审计

讲解有关合约审计的相关知识,让你能熟练一名合约审计员所需要的掌握的知识点。传授给你最高单日审核10+合约的程序员的所有知识。

匿名性研究 讲解区块链匿名性相关实现方案及实战。
实战大流量交易所钱包设计 讲解交易所钱包相关设计,涵盖安全、用户意图分析、交易加速等。
交易所难点突破 包含交易所业务与架构选型、分布式撮合引擎实战等内容。
区块浏览器 讲解如何搭建区块浏览器、动辄百GB的区块数据,如何快速检索等内容。
区块链钱包 包含有关钱包的设计等内容。
其他 包含区块链发展历史,主流币趋势分析等内容。

参考文献:

维基 — 散列函数

维基 — SHA-2算法

SHA-256原理

Java工具库 — HuTool

     《区块链底层设计Java实战》

发布了21 篇原创文章 · 获赞 11 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/weixin_38652136/article/details/105247025