Search code examples
c#cryptographybouncycastlecryptocurrencysecp256k1

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


I have this nodejs snippet which I want to implement in C#. The SignTransaction function takes in a seedHex string (length: 64) and a TransactionHex string, a signature is created using secp256k1 algorithm and appended to the end of the 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');
}

example transaction hex

01efa5060f5fdae5ed9d90e5f076b2c328a64e347d0c87e8e3317daec5a44fe7c8000102bd53e48625f49e60ff6b7a934e3871b54cc2a93a8737352e8320549e42e2322bab0405270000167b22426f6479223a2248656c6c6f20576f726c64227de807d461f4df9efad8a0f3f716002102bd53e48625f49e60ff6b7a934e3871b54cc2a93a8737352e8320549e42e2322b0000

example signed transaction hex returned from the javascript function

01efa5060f5fdae5ed9d90e5f076b2c328a64e347d0c87e8e3317daec5a44fe7c8000102bd53e48625f49e60ff6b7a934e3871b54cc2a93a8737352e8320549e42e2322bab0405270000167b22426f6479223a2248656c6c6f20576f726c64227de807d461f4df9efad8a0f3f716002102bd53e48625f49e60ff6b7a934e3871b54cc2a93a8737352e8320549e42e2322b00473045022100c36e2b80f2160304cf640b1296244e7a3873aacf2831098bca3727ad06f4c270022007d3697ceef266a05ad70219d92fbd570f6ec7a5731aaf02718213067d42d1cf

If you notice the signed hex is TransactionHex + 47 (length of signature) + 473045022100c36e2b80f2160304cf640b1296244e7a3873aacf2831098bca3727ad06f4c270022007d3697ceef266a05ad70219d92fbd570f6ec7a5731aaf02718213067d42d1cf (actual signature)

I am trying to use C# Bouncy Castle library in Unity3d to generate the signature part but no success.

This is the C# code (source)

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();
    }

This does generate a signature but it is not identical to the one I mentioned above.

Signature generated from GetSignature

3044022018a06b1f2b8a1e2f5ea2a78b0d6d98ec483b9fa4345821cfef892be6c825ff4702207958160e533c801dff5e50600206e1cd938d6df74ebfa4b0347a95de67dda986

P.S. Here is the python equivalent of signing transactions for reference.


Solution

  • sha256.x2() in the NodeJS code hashes twice, while ECDsaSigner in the C# code does not hash at all. Therefore, in the C# code, GetSignature() must explicitly hash twice:

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

    with

    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;
    }
    

    Since both codes use the deterministic ECDSA algorithm according to RFC6979, the hashes for the same input data are identical and can be compared.

    The overall result is the concatenation of txnHex without the last byte, the hex encoded length of the signature and the signature in ASN.1/DER format; a possible implementation is:

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

    Note that the uvarint64ToBuf() implementation is actually more complex, but can be simplified for the signature lengths used here.