虽然知道以太坊的多重签名机制是通过合约实现的,但一直没去仔细看过,近期在GUSD中也看到这部分内容,按操作来熟悉一下,更底层机制有时间再去研究。
公私钥对和地址
要做离线签名,首先得有私钥,有了私钥就能算出公钥和地址,代码如下:
let elliptic = require('elliptic');
let ec = new elliptic.ec('secp256k1');
let ethUtil = require('ethereumjs-util');
// 私钥->公钥
let keyPair = ec.genKeyPair();
// let keyPair = ec.keyFromPrivate("eb4e675d4adee4fa60cf82a681a7d192d7bd3ece7938e7e63ea06a5259eb0d0c");
let privKey = keyPair.getPrivate("hex");
let pubKey = keyPair.getPublic();
console.log(`Private key: ${privKey}`);
console.log("Public key :", pubKey.encode("hex").substr(2));
console.log("Public key (compressed):",
pubKey.encodeCompressed("hex"));
// 私钥->公钥->地址
let publicKey = ethUtil.privateToPublic(new Buffer(privKey, 'hex'));
// console.log("Public key:", publicKey)
let addr ="0x"+ ethUtil.publicToAddress(publicKey).toString('hex');
console.log("Ether addr:", addr);
每次执行都会生成不同的公私钥对和地址,如下:
Private key: a3e5a66cc6a76c23ea4e827bb7bccbf1c90f67f51fa1f4c1a2433e856fe7ae3e
Public key : 9ae7020d6344cc5876f3aaf34e8fdbbd14e162a07e091655062dc4af3aa1ad53cbcd787811b5d367692129650fb14d9bb387a652dc2d186924dc0c82f06c4187
Public key (compressed): 039ae7020d6344cc5876f3aaf34e8fdbbd14e162a07e091655062dc4af3aa1ad53
Ether addr: 0x40e1843610d41853cbe22425e2ca2b2fb5538559
我们再生成一个公私钥对用来做签名验证:
Private key: f5b37f8daa631f49e64124b41a1f4768b6e755fc906781614024772710711cf0
Public key : fc184f3ac0418e2f5dda56a9f4e021dd174afb8d84a9014ae6c2851a121edbd250d460b07cac6e381ea9520b727d10e6a0e4c15a02c010fd754286f77c0a4665
Public key (compressed): 03fc184f3ac0418e2f5dda56a9f4e021dd174afb8d84a9014ae6c2851a121edbd2
Ether addr: 0x10f505c5077fb9e5ae06b9f9e98a4f037e54e389
合约验证签名
我们目的是实现离线签名,然后在合约中进行验证,合约如下:
pragma solidity ^0.4.21;
contract MultiSign {
mapping (address => bool) public signerSet;
function MultiSign(
address[] _signers
)
public
{
for (uint i = 0; i < _signers.length; i++) {
require(_signers[i] != address(0));
signerSet[_signers[i]] = true;
}
}
// 取签名公钥
function verify_addr(bytes32 _message, uint8 _v, bytes32 _r, bytes32 _s) public constant returns (address addr) {
address signer = ecrecover(_message, _v, _r, _s);
addr = signer;
return signer;
}
function verify_one(bytes32 _message, uint8 _v, bytes32 _r, bytes32 _s) public constant returns (bool success) {
address signer = ecrecover(_message, _v, _r, _s);
success = signerSet[signer];
return success;
}
}
创建合约时先要设置好允许的签名地址,验证时只要通过签名数据得出地址在允许的签名地址列表中,签名就通过。
发布合约,初始化时指定前面生成的两个以太坊地址,生成abi
abi_mulsign = [...]
addr_mulsign = "0x230993f4cd49203df242f4f02ef3374beec0f55b"
contract_mulsign = eth.contract(abi_mulsign).at(addr_mulsign)
需要签名的数据为我的steemit网址:https://steemit.com/@chaimyu
用地址“0x10f505c5077fb9e5ae06b9f9e98a4f037e54e389”对数据进行签名,如下:
Msg: https://steemit.com/@chaimyu
Msg hash: b1de4ffbedd11df4af08fc63d9585ed5cb05aaef754d6db0c5a37dbc6c8caf7f
Signature: Signature {
r: <BN: 235078c22debd6608de5a2d8bf8f9ff210a4605a28644b6ac219872943c735bc>,
s: <BN: 29e96c39ef293832de3276edd0df44239e1204f78d4879455ce675275e6e1382>,
recoveryParam: 1 }
为了方便验证,加了一个根据签名信息取以太坊地址的函数,如下:
> contract_mulsign.verify_addr.call("0xb1de4ffbedd11df4af08fc63d9585ed5cb05aaef754d6db0c5a37dbc6c8caf7f", 28, "0x235078c22debd6608de5a2d8bf8f9ff210a4605a28644b6ac219872943c735bc", "0x29e96c39ef293832de3276edd0df44239e1204f78d4879455ce675275e6e1382")
"0x10f505c5077fb9e5ae06b9f9e98a4f037e54e389"
调用合约验证函数:
> contract_mulsign.verify_one.call("0xb1de4ffbedd11df4af08fc63d9585ed5cb05aaef754d6db0c5a37dbc6c8caf7f", 28, "0x235078c22debd6608de5a2d8bf8f9ff210a4605a28644b6ac219872943c735bc", "0x29e96c39ef293832de3276edd0df44239e1204f78d4879455ce675275e6e1382")
true
多重签名
先用一私钥验证一下,签名数据:
Msg: https://steemit.com/@chaimyu
Msg hash: b1de4ffbedd11df4af08fc63d9585ed5cb05aaef754d6db0c5a37dbc6c8caf7f
Signature: Signature {
r: <BN: 9f2e39d8a678797e2f6dc37b3170111420bb5cb43bccec83a83a7e331519ef85>,
s: <BN: 4d2e05baba9f7d384c9fbe8e4fa0a5633f9dd4cb6bbb6b2d4e53d7b0c97e79e3>,
recoveryParam: 1 }
验证地址:
> contract_mulsign.verify_addr.call("0xb1de4ffbedd11df4af08fc63d9585ed5cb05aaef754d6db0c5a37dbc6c8caf7f", 28, "0x9f2e39d8a678797e2f6dc37b3170111420bb5cb43bccec83a83a7e331519ef85", "0x4d2e05baba9f7d384c9fbe8e4fa0a5633f9dd4cb6bbb6b2d4e53d7b0c97e79e3")
"0x40e1843610d41853cbe22425e2ca2b2fb5538559"
有了验证单个签名的方法,而且已经对单个签名做过验证,同样的可以在函数中验证几个离线签名数据,合约中增加以下函数:
function verify_two(bytes32 _message, uint8 _v1, bytes32 _r1, bytes32 _s1, uint8 _v2, bytes32 _r2, bytes32 _s2) public constant returns (bool success) {
address signer1 = ecrecover(_message, _v1, _r1, _s1);
address signer2 = ecrecover(_message, _v2, _r2, _s2);
success = signerSet[signer1] && signerSet[signer2];
return success;
}
通过离线签名工具用上面的两个私钥签名数据,然后调用此函数:
> contract_mulsign.verify_two.call("0xb1de4ffbedd11df4af08fc63d9585ed5cb05aaef754d6db0c5a37dbc6c8caf7f", 28, "0x235078c22debd6608de5a2d8bf8f9ff210a4605a28644b6ac219872943c735bc", "0x29e96c39ef293832de3276edd0df44239e1204f78d4879455ce675275e6e1382",28, "0x9f2e39d8a678797e2f6dc37b3170111420bb5cb43bccec83a83a7e331519ef85", "0x4d2e05baba9f7d384c9fbe8e4fa0a5633f9dd4cb6bbb6b2d4e53d7b0c97e79e3")
true
既然可以实现2/N签名机制,当然也可以在合约创建时传入需要签名个数,实现M/N签名机制。
recoveryParam为0的无法验签?
在测试过程中碰到recoveryParam为0的按同样方式签名,验签时生成的地址不对,是27那个值用错了?谁帮解答下。
公私钥
Private key: f923b04356555667fb945dd44b6cb7a188f549d839345768c149ac791ba05707
Public key : 372f0fa3247519f5ab4c349d47902b72bd89e383f13109f3b4b8c890ca6c533db27e641377efeb53bc1d14974d56e419c36c4a4d0ae9b165d3511f2c600ef68e
Public key (compressed): 02372f0fa3247519f5ab4c349d47902b72bd89e383f13109f3b4b8c890ca6c533d
Ether addr: 0x1c2d77e42c3d47ab24d6021fa14b16571d3038f7
签名数据
Msg hash: b1de4ffbedd11df4af08fc63d9585ed5cb05aaef754d6db0c5a37dbc6c8caf7f
Signature: Signature {
r: <BN: a0fe7c7d607db4a45be9c343a57c285a59a16d01af2cfbf3768e232c55cbb7c7>,
s: <BN: 6cdb7534118632437ef73e95dc7269f0bc24ad15fcc72fb6bff666add264923>,
recoveryParam: 0 }
验证
> contract_mulsign.verify_addr.call("0xb1de4ffbedd11df4af08fc63d9585ed5cb05aaef754d6db0c5a37dbc6c8caf7f", 27, "0xa0fe7c7d607db4a45be9c343a57c285a59a16d01af2cfbf3768e232c55cbb7c7", "0x6cdb7534118632437ef73e95dc7269f0bc24ad15fcc72fb6bff666add264923")
>
"0xa1ca20364a4e1e374cdafce9850dfd16791543f0"
1129终于找到验证签名失败的原因了,你找到了吗?