How to sign a transaction offline?

I have a test demo by using bouncyCastle.

    @Test
    public void testBouncyCastleSecp256k1() {
        X9ECParameters p = SECNamedCurves.getByName("secp256k1");
        ECDomainParameters params = new ECDomainParameters(p.getCurve(), p.getG(), p.getN(), p.getH());
        BigInteger privKey = new BigInteger("b77e91f2b6905bd0cb7295c529b20152181b5e9e18f62c02b3c2d75747a73f1e", 16);
        BigInteger pubKey = new BigInteger("02d7482187387eef4d462864fe0f41a6780cfb25d7376b5535b6af8640d97281d3", 16);
        String msg = "041f2b145c7fa7d8b49de0e9de59fb96a270b3b1869b1d1cbf2059a560542b2c";

        ECPrivateKeyParameters privParams = new ECPrivateKeyParameters(privKey, params);
        byte[] privateKeyBytes = ECKeyUtils.adjustArray(privParams.getD().toByteArray(), 32);
        System.out.println("privKey : " + Utils.HEX.encode(privateKeyBytes));

        byte[] messageBytes = HashUtils.sha256(msg.getBytes()).asBytes();
        ECDSASigner signer = new ECDSASigner(new HMacDSAKCalculator(new SHA256Digest()));
        signer.init(true, privParams);
        BigInteger[] signature = signer.generateSignature(messageBytes);
        byte[] derBytes = ECKeyUtils.toUnrecoverableDERBytes(signature[0], signature[1]);
        System.out.println("Signature(DER format): " + Utils.HEX.encode(derBytes));

        BouncyCastleKeyHandler handler = new BouncyCastleKeyHandler(CustomNamedCurves.getByName("secp256k1"));
        if (handler.verify(messageBytes, signature[0], signature[1], pubKey.toByteArray())) {
            System.out.println("verifySignature successfully");
        } else {
            System.out.println("verifySignature failed");
        }
    }

And it returns the signature in DER format :Signature(DER format): 30440220079ad26fdbf47ef41659e32a39b1984b486ff609205e8b4914e41bc51fa47a2902207f9dd5f5c06aee3f4946e6db7160b7f7099b9b2fbb0c632259cea1ea12a577a8
截屏2022-07-27 14.42.44

But it always return the error “Unable to calculate V byte for public key” when I use this signature to finalize transaction .
截屏2022-07-27 09.54.45

When I use the same private key to sign the transaction by python lib and finalize transaction, it returns the correct signed transaction and transaction identifier info.
截屏2022-07-27 14.44.01

Where do BouncyCastleKeyHandler, ECKeyUtils and HashUtils come from? I can’t find them in BouncyCastle.

Also, the DER signature differs from what I get (UPD: the signature might vary because it is non-deterministic):

package live.radix.example;

import com.radixdlt.crypto.ECDSASignature;
import com.radixdlt.crypto.ECKeyPair;
import com.radixdlt.crypto.ECKeyUtils;
import com.radixdlt.crypto.exception.PrivateKeyException;
import com.radixdlt.crypto.exception.PublicKeyException;
import com.radixdlt.utils.Bytes;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1OutputStream;
import org.bouncycastle.asn1.DLSequence;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.junit.Test;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;

import static org.junit.Assert.assertEquals;

public class SignerTest {

    @Test
    public void testBouncyCastleSecp256k1_2() throws PrivateKeyException, PublicKeyException, DecoderException, IOException {
        BigInteger privKey = new BigInteger("b77e91f2b6905bd0cb7295c529b20152181b5e9e18f62c02b3c2d75747a73f1e", 16);
        String msg = "041f2b145c7fa7d8b49de0e9de59fb96a270b3b1869b1d1cbf2059a560542b2c";

        ECPrivateKeyParameters privParams = new ECPrivateKeyParameters(privKey, ECKeyUtils.domain());
        byte[] privateKeyBytes = ECKeyUtils.adjustArray(privParams.getD().toByteArray(), 32);
        ECKeyPair keyPair = ECKeyPair.fromPrivateKey(privateKeyBytes);

        assertEquals("02d7482187387eef4d462864fe0f41a6780cfb25d7376b5535b6af8640d97281d3", keyPair.getPublicKey().toHex());

        byte[] bytes = Hex.decodeHex(msg);
        byte[] derSignature = toDerSignature(keyPair.sign(bytes));

        System.out.println(Bytes.toHexString(derSignature));
    }

    private byte[] toDerSignature(ECDSASignature sign) throws IOException {
        var os = new ByteArrayOutputStream();
        var asn1OutputStream = ASN1OutputStream.create(os);
        asn1OutputStream.writeObject(
                new DLSequence(
                        new ASN1Encodable[]{new ASN1Integer(sign.getR()), new ASN1Integer(sign.getS())}));
        return os.toByteArray();
    }
}

(requires the below dependency)

<!-- https://mvnrepository.com/artifact/live.radix/radixdlt-java-common -->
<dependency>
    <groupId>live.radix</groupId>
    <artifactId>radixdlt-java-common</artifactId>
    <version>1.2.1</version>
</dependency>

This is toUnrecoverableDERBytes function detail in my test.

public static byte[] toUnrecoverableDERBytes(BigInteger r, BigInteger s) {
        final ByteArrayOutputStream os = new ByteArrayOutputStream();
        final ASN1OutputStream asn1OutputStream = ASN1OutputStream.create(os);
        try {
            asn1OutputStream.writeObject(
                    new DLSequence(new ASN1Encodable[]{new ASN1Integer(r), new ASN1Integer(s)}));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return os.toByteArray();
    }

r and s are both generate by ECDSA signing progess, so I passed them in directly as parameters

    var signer =
        new ECDSASigner(
            useDeterministicSignatures
                ? new HMacDSAKCalculator(new SHA256Digest())
                : new RandomDSAKCalculator());

    signer.init(true, new ECPrivateKeyParameters(new BigInteger(1, privateKey), domain));

    var components = signer.generateSignature(hash);
    var r = components[0];
    var s = components[1];

My private key and public key is not genrated by ECKeyPair in radixdlt-java-common, but I think it should not effect.

ok, so the signing part looks ok, can you please share the code that you use to build the TransactionFinalizeRequest to the API ? in particular creating the signature param.

BouncyCastleKeyHandler, ECKeyUtils and HashUtils are a part I extracted from radix-java-common

This is my API request send to url : {{dev_node}}/transaction/finalize

{
    "network_identifier": {
        "network": "mainnet"
    },
    "unsigned_transaction": "079da6d7ef3f9465e0e3260986098c6c50fe40fe0a1b0fc0d4c2c101821ce563b300000001010021000000000000000000000000000000000000000000000000000094df774aab000002004506000402f01ceafea2166eac118485daf14e2afa75caddfe716fc92ef6a1eaa41953561e01000000000000000000000000000000000000000000000000e23a3d3a66a157850008000002004506000402f01ceafea2166eac118485daf14e2afa75caddfe716fc92ef6a1eaa41953561e01000000000000000000000000000000000000000000000000e2363fdd968c2785020045060004024fa1f2474d91251ae938b46e35a6242ccb8f6d5f55e585f0e72a41afadba999b010000000000000000000000000000000000000000000000000003fd5cd015300000",
    "signature": {
        "public_key": {
            "hex": "02f01ceafea2166eac118485daf14e2afa75caddfe716fc92ef6a1eaa41953561e"
        },
        "bytes": "3045022100df285555c4734f081648e3e35298abba672fb84900103bf24102e8b7cef352cc02201722c268e8e8989fa30e10993cc1c739e6dbd4578063211d6a672b9e6da01e7e"
    },
    "submit": "false"
}

public_key.hex here differs from your above example. is this request for a different address?

Yes, this is a recent request

Thank you for your reply .

I use the wrong decode function and one more time sha256 operation.

byte[] messageBytes = HashUtils.sha256(msg.getBytes()).asBytes();

should change to

byte[] bytes = Hex.decodeHex(msg);
2 Likes