掘金 后端 ( ) • 2024-04-29 16:36

随机私钥

我们都知道,私钥其实就是256位的随机数字,那我们能否自己编一个256位的随机数然后作为私钥,再去计算他的公钥和地址,自己使用这个用来进行交易呢?

答案是没问题的,并且我们在创建该私钥的时候还可以将一些自己喜欢的信息添加到里面,例如自己的生日啊,喜欢的数字啊,等等一些有意义的东西加载私钥里面,都是可以用的,只是说可能因为具有了某些确定的信息数字,了解你的人可以根据此信息推导你的私钥,但是这都没什么意义哈,虽然理论上私钥安全性降低了,但是依然很难破解,并且转成方便表示的64位16进制数字表示,只要不是太过分把里面一半多的数字都换成你喜欢的某段数字序列,基本就没有必要担心被破解的可能。

因为理论上宇宙中的原子数量为10的80次方到82次方。虽然这个估计值也非常庞大,但和私钥的个数相比,它仍然小得多。这种对比可以帮助我们理解,为什么使用256位的数字作为加密密钥在技术上提供了如此高的安全性——因为与之匹配的组合数量超出了常规想象。

已知私钥,计算一个以太坊地址

可以写一个自己喜欢的私钥,例如,我写一个64的16进制数:

0x555555222222220000000011111111333333331111111144444444aaaaaaaa

参考私钥及地址:

  • Private Key: 0x1ab42cc412b618bdea3a599e3c9bae199ebf030895b039e9db1e30dafb12b727
  • Address: 0x9858EfFD232B4033E47d90003D41EC34EcaEda94
  • 参考链接: https://github.com/ethereumbook/ethereumbook/blob/develop/04keys-addresses.asciidoc

计算步骤

那好了,问题有了,如何从一个256位随机数计算出一个以太坊地址呢?其实并不是很复杂,主要就三步

  • 从随机数计算公钥
  • 从公钥计算地址
  • 从地址计算eip55格式地址

可能大家不知道这个eip55地址有什么用哈,下面解释一下: EIP-55,或称为Ethereum Improvement Proposal 55,是以太坊社区提出的一项改进提案,旨在增强以太坊地址的可读性和错误检测能力。EIP-55格式的地址通过大小写敏感的校验和机制来减少用户在手动输入或复制地址时产生的错误。

从随机数计算公钥

那怎么用随机数计算公钥呢?在区块链世界中,有许许多多不同的链,实际上每个链的从私钥到公钥的生成也不尽相同,拿我们最熟知的比特币和以太坊来说,他俩都使用了同一种椭圆曲线叫做secp256k1和ECDSA签名算法计算公钥。

并且基本上,作为区块链技术的底层技术,基本在各种语言里面都有很好的库去实现ECDSA和secp256k1,接下来,我们就使用 k256 库来实现从私钥到公钥的计算。

//函数参数为u8类型数组,8*32代表256位私钥
fn getpubkey_from_private_key(private_key: &[u8; 32]) -> String {
   //引入需要的包
   use k256::{elliptic_curve::sec1::ToEncodedPoint, SecretKey};
   //从参数获取私钥
   let secret_key = SecretKey::from_slice(private_key).expect("secret key err");
   //获取公钥,在这里要说一下,其实公钥是椭圆曲线上的一个点,具有x,y值
   let public_key = secret_key.public_key().to_encoded_point(false);
   //获取公钥的x点
   let x = hex::encode(public_key.x().unwrap().as_slice());
   获取公钥的y点
   let y = hex::encode(public_key.y().unwrap().as_slice());
   //将其表示为16进制格式并添加0x04前缀
   format!("0x04{}{}", x, y)
}

好了,通过以上方式我们就可以从我们自定义的私钥获取私钥了,

从公钥计算地址

相对比特币来说,以太坊的地址计算就简单很多,以太坊只需要对公钥进行 Keccak-256 哈希计算之后,取后20个字节,也就是后40位16进制数字并且添加 0x 前缀即可,我们在这里需要使用 sha3 中 Keccak256 实现。

//参数为64个u8类型的512位的公钥
fn get_eth_address_from_pubkey(public_key: &[u8; 64]) -> String {
   //引入包
   use sha3::{Digest, Keccak256};
   //计算公钥哈希
   let mut hasher = Keccak256::new();
   hasher.update(public_key);
   let hash = hasher.finalize();
   //将后面的 20 字节 转为16进制格式并添加 0x 前缀
   format!("0x{}", hex::encode(hash)[24..].to_owned())
}

然后我们就完成了这个函数的编写,有读者可能会疑惑为什么是 24.. 切片,是因为 hash 是用 64 个 16进制数表示的,我们取后面的40位,也就是从第25位到64位,在这个地方 256位哈希 = 64个16进制数 = 32个u8类型数组,它们之间的转换一定要清楚,256是0和1表示的bit,1个16进制数4 bit,一个u8类型8 bit ,一个字节

从地址计算eip55格式地址

在EIP-55格式中,一个以太坊地址将包括大写字母和小写字母,这些字母的大小写不是随机的,而是根据地址本身的哈希值来确定。这样,如果地址中的任何字符由于误操作或其他原因而被错误地改变大小写,就会导致校验和验证失败,从而提醒用户地址可能已经被误输入。

  • 首先,你需要一个以太坊地址的小写形式,不包括前缀 "0x"。

  • 对这个小写地址字符串计算Keccak-256哈希值。这一步是生成校验用数据的关键。

  • 通过比较地址的每个字符与哈希值中相应位置的字符,决定该字符应该是大写还是小写。

  • 对地址中的每个字符(从左到右):

    • 如果字符是数字,则保持不变。

    • 如果字符是字母,将相应位置的哈希值的字符转换为十六进制数(因为哈希是字节形式,每个字节可以表示为两个十六进制字符)。查看此十六进制数的高四位(即哈希字符的前半部分):

      • 如果高四位大于等于8,则该地址中的对应字母应该是大写。
      • 如果小于8,则保持小写。
  • 将处理后的字符连成字符串,加上前缀 "0x",即得到带校验和的以太坊地址。

话不多说,上代码:

//参数为20个u8类型的地址
fn eip_55(address: &[u8; 20]) -> String {
    //导入包
    use sha3::{Digest, Keccak256};
    //取地址哈希
    let mut hasher = Keccak256::new();
    hasher.update(hex::encode(address));
    let hash = hasher.finalize();
    let hash = hex::encode(hash).to_owned();
    let address = hex::encode(address);
    //将地址与地址哈希对比,修改地址中的字母大小写
    let address = address
        .chars()
        .enumerate()
        .map(|(i, char)| {
            if char.is_ascii_lowercase() && hash.chars().nth(i).unwrap_or('0') >= '8' {
                char.to_ascii_uppercase()
            } else {
                char
            }
        })
        .collect::<String>();
        //格式化输出
    format!("0x{}", address)
}

好了,到现在为止,我们就获得了一个可以校验的以太坊地址了,这样在输入的时候就可以有效防止输入错误的地址而损失财产,最后我们一起来测试一下吧

 #[test]
 fn test_gen_eth_address_from_private_key() { 
 
    // get private key
    let private_key =
        "0xf8f8a2f43c8376ccb0871305060d7b27b0554d2cc72bccf41b2705608452f315".to_owned();
    let private_key_whihout_prefix = private_key.trim_start_matches("0x");
    let public_key = getpubkey_from_private_key(
        hex::decode(private_key_whihout_prefix)
            .expect("decode err")
            .as_slice()
            .try_into()
            .expect("private_key len err"),
    );

    assert_eq!(
        public_key,
        "0x046e145ccef1033dea239875dd00dfb4fee6e3348b84985c92f103444683bae07b83b5c38e5e2b0\
    c8529d7fa3f64d46daa1ece2d9ac14cab9477d042c84c32ccd0"
            .to_owned()
    );
    
    //get eth address
    let public_key_whihout_prefix = public_key.trim_start_matches("0x04");
    let eth_address = get_eth_address_from_pubkey(
        hex::decode(public_key_whihout_prefix)
            .expect("decode err")
            .as_slice()
            .try_into()
            .expect("public_key len err"),
    );

    assert_eq!(
        eth_address,
        "0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9".to_owned()
    );

    //get eip55 eth address
    let eth_address_whihout_prefix = eth_address.trim_start_matches("0x");
    let eip55_eth_address = eip_55(
        hex::decode(eth_address_whihout_prefix)
            .expect("decode err")
            .as_slice()
            .try_into()
            .expect("eth_address len err"),
    );
    assert_eq!(
        eip55_eth_address,
        "0x001d3F1ef827552Ae1114027BD3ECF1f086bA0F9".to_owned()
    );

    //test example
    assert_eq!(
        get_eip55_eth_address_from_private_key(
            "0x1ab42cc412b618bdea3a599e3c9bae199ebf030895b039e9db1e30dafb12b727".to_owned()
        ),
        "0x9858EfFD232B4033E47d90003D41EC34EcaEda94".to_owned()
    );
}

没有问题,通过了一个测试数据,说明我们的方法是对的,接下来,我们把方法放在一起,直接实现一个从私钥获得地址的方法:

fn get_eip55_eth_address_from_private_key(private_key: String) -> String {
    //get public key
    let private_key_whihout_prefix = private_key.trim_start_matches("0x");
    let public_key = getpubkey_from_private_key(
        hex::decode(private_key_whihout_prefix)
            .expect("decode err")
            .as_slice()
            .try_into()
            .expect("private_key len err"),
    );

    //get eth address
    let public_key_whihout_prefix = public_key.trim_start_matches("0x04");
    let eth_address = get_eth_address_from_pubkey(
        hex::decode(public_key_whihout_prefix)
            .expect("decode err")
            .as_slice()
            .try_into()
            .expect("public_key len err"),
    );

    //get eip55 eth address
    let eth_address_whihout_prefix = eth_address.trim_start_matches("0x");

    eip_55(
        hex::decode(eth_address_whihout_prefix)
            .expect("decode err")
            .as_slice()
            .try_into()
            .expect("eth_address len err"),
    )
}

测试一下:

#[test]
fn test() {
    let address = get_eip55_eth_address_from_private_key(
        "0x3ac5dc9a32f4db6501f7fc01f61961e4c30efbf46f01ad73c09c113bb678e60b".to_owned(),
    );
    assert_eq!(address, "0x18a5113F7FBC31E73c2bB38a895FD6683803E7F8");
}

好了没有问题,接下来我们就可以把最初你生成的私钥放进去看看能获得什么样的地址吧。 完整详细代码:https://github.com/ChainThemAll/bitcoin_example_rust/blob/master/src/address.rs Pomelo_刘金。转载请注明原文链接。感谢!