如何使用带有 Bouncy Castle 的 C# 中的 secp256k1 使用 SeedHex 签署 TransactionHex?

How do I sign a TransactionHex with SeedHex using secp256k1 in C# with Bouncy Castle?

我有这个 nodejs 片段,我想在 C# 中实现它。 SignTransaction 函数接受一个 seedHex 字符串(长度:64)和一个 TransactionHex 字符串,使用 secp256k1 算法创建签名并附加到 TransactionHex 的末尾。

const sha256 = require('sha256');
const EC = require('elliptic').ec;
const ec = new EC("secp256k1");

// Serialize a number into an 8-byte array. This is a copy/paste primitive, not worth
// getting into the details.
function uvarint64ToBuf (uint) {
    const result = [];

    while (uint >= 0x80) {
        result.push((uint & 0xFF) | 0x80);
        uint >>>= 7;
    }

    result.push(uint | 0);

    return new Buffer(result);
}

// Sign transaction with seed
function signTransaction (seed, txnHex) {
    const privateKey = ec.keyFromPrivate(seed);
    const transactionBytes = new Buffer(txnHex, 'hex');
    const transactionHash = new Buffer(sha256.x2(transactionBytes), 'hex');
    const signature = privateKey.sign(transactionHash);
    const signatureBytes = new Buffer(signature.toDER());
    const signatureLength = uvarint64ToBuf(signatureBytes.length);
    const signedTransactionBytes = Buffer.concat([
        transactionBytes.slice(0, -1),
        signatureLength,
        signatureBytes
    ])
    return signedTransactionBytes.toString('hex');
}

十六进制交易示例

01efa5060f5fdae5ed9d90e5f076b2c328a64e347d0c87e8e3317daec5a44fe7c8000102bd53e48625f49e60ff6b7a934e3871b54cc2a93a8737352e8320549e42e2322bab0405270000167b22426f6479223a2248656c6c6f20576f726c64227de807d461f4df9efad8a0f3f716002102bd53e48625f49e60ff6b7a934e3871b54cc2a93a8737352e8320549e42e2322b0000

从 javascript 函数返回的十六进制签名交易示例

01efa5060f5fdae5ed9d90e5f076b2c328a64e347d0c87e8e3317daec5a44fe7c8000102bd53e48625f49e60ff6b7a934e3871b54cc2a93a8737352e8320549e42e2322bab0405270000167b22426f6479223a2248656c6c6f20576f726c64227de807d461f4df9efad8a0f3f716002102bd53e48625f49e60ff6b7a934e3871b54cc2a93a8737352e8320549e42e2322b00473045022100c36e2b80f2160304cf640b1296244e7a3873aacf2831098bca3727ad06f4c270022007d3697ceef266a05ad70219d92fbd570f6ec7a5731aaf02718213067d42d1cf

如果您注意到带符号的十六进制是 TransactionHex + 47 (length of signature) + 473045022100c36e2b80f2160304cf640b1296244e7a3873aacf2831098bca3727ad06f4c270022007d3697ceef266a05ad70219d92fbd570f6ec7a5731aaf02718213067d42d1cf (actual signature)

我正在尝试使用 Unity3d 中的 C# Bouncy Castle 库来生成签名部分,但没有成功。

这是 C# 代码()

public string GetSignature(string privateKey, string message)
    {
        var curve = SecNamedCurves.GetByName("secp256k1");
        var domain = new ECDomainParameters(curve.Curve, curve.G, curve.N, curve.H);
        var keyParameters = new ECPrivateKeyParameters(new BigInteger(privateKey, 16), domain);

        var signer = new ECDsaSigner(new HMacDsaKCalculator(new Sha256Digest()));
        signer.Init(true, keyParameters);

        var signature = signer.GenerateSignature(Encoding.UTF8.GetBytes(message));

        var r = signature[0];
        var s = signature[1];

        var otherS = curve.Curve.Order.Subtract(s);

        if (s.CompareTo(otherS) == 1)
        {
            s = otherS;
        }

        var derSignature = new DerSequence
        (
            new DerInteger(new BigInteger(1, r.ToByteArray())),
            new DerInteger(new BigInteger(1, s.ToByteArray()))
        )
        .GetDerEncoded();

        return Convert(derSignature);
    }

    public string Convert(byte[] input)
    {
        return string.Concat(input.Select(x => x.ToString("x2")));
    }

    public byte[] Convert(string input)
    {
        if (input.StartsWith("0x")) input = input.Remove(0, 2);

        return Enumerable.Range(0, input.Length / 2).Select(x => System.Convert.ToByte(input.Substring(x * 2, 2), 16)).ToArray();
    }

这确实生成了一个签名,但它与我上面提到的不同。

签名生成自GetSignature

3044022018a06b1f2b8a1e2f5ea2a78b0d6d98ec483b9fa4345821cfef892be6c825ff4702207958160e533c801dff5e50600206e1cd938d6df74ebfa4b0347a95de67dda986

P.S。 Here 相当于 python 签署交易以供参考。

NodeJS 代码中的

sha256.x2() 散列两次,而 C# 代码中的 ECDsaSigner 根本不散列。因此,在C#代码中,GetSignature()必须显式散列两次:

using Org.BouncyCastle.Crypto.Digests;
...
var messageBytes = Convert(message);
var hash = GetHash(GetHash(messageBytes));
var signature = signer.GenerateSignature(hash);
...

private static byte[] GetHash(byte[] data)
{
    var digest = new Sha256Digest();
    var hash = new byte[digest.GetDigestSize()];
    digest.BlockUpdate(data, 0, data.Length);
    digest.DoFinal(hash, 0);
    return hash;
}

由于两种代码都使用了符合 RFC6979 的确定性 ECDSA 算法,因此相同输入数据的哈希值是相同的,可以进行比较。

整体结果是txnHex没有最后一个字节,签名的十六进制编码长度和ASN.1/DER格式的签名的串联;一个可能的实现是:

var txnHex = "...";
var seed = "...";
string signature = GetSignature(seed, txnHex);
string signedTransactionBytes = txnHex.Substring(0, txnHex.Length - 2) + (signature.Length / 2).ToString("x") + signature;

请注意,uvarint64ToBuf() 实现实际上更复杂,但可以针对此处使用的签名长度进行简化。