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

But it always return the error “Unable to calculate V byte for public key” when I use this signature to finalize transaction .

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.

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