So i'm working with the altcoin denarius which has the following raw TX structure. This is a transaction I am try to sign:
Original:
01000000a73a7b5d01068ee1e392668191ce0ae4337074e7a384be3c5d72bd45b7f7ce6fa2006258110100000000ffffffff0208c29400000000001976a914ebb93cbd9952b3941e0415509b301dfd1cb84b0388ac90d00300000000001976a914b1acab9b97a1d9f35898b9d74fe11619e636ac3d88ac00000000
VERSION: 01000000
TIME: a73a7b5d
INPUT COUNT: 01
INPUT [0] PREV TXID: 068ee1e392668191ce0ae4337074e7a384be3c5d72bd45b7f7ce6fa200625811
INPUT [0] PREV OUTPUT INDEX: 01000000
INPUT [0] SCRIPT LENGTH: 00
INPUT [0] SEQUENCE NUMBER: ffffffff
NUMBER OF OUTPUTS:02
OUTPUT [0] VALUE: 08c2940000000000
OUTPUT [0] PUBLIC KEY SCRIPT LENGTH: 19 (25 in decimal)
OUTPUT [0] PUBLIC KEY SCRIPT:
76 (OP_DUP)
a9 (OP_HASH160)
14 (bytes to push)
SCRIPT HASH: ebb93cbd9952b3941e0415509b301dfd1cb84b03 (pubkey hash)
88 (OP_EQUALVERIFY)
ac (OP_CHECKSIG)
OUTPUT [1] VALUE: 90d0030000000000
OUTPUT [1] PUBLIC KEY SCRIPT LENGTH: 19 (25 in decimal)
OUTPUT [1] PUBLIC KEY SCRIPT:
76 (OP_DUP)
a9 (OP_HASH160)
14 (bytes to push)
SCRIPT HASH: b1acab9b97a1d9f35898b9d74fe11619e636ac3d (pubkey hash)
88 (OP_EQUALVERIFY)
ac (OP_CHECKSIG)
LOCKTIME: 00000000
I have various methods to convert bytes to hex strings and a few others:
public static String hex2smallEndian(String hex){
String[] dat = split(hex, 2);
String swapped = "";
for(int i = dat.length - 1; i >= 0; i--){
swapped += dat[i];
}
return swapped;
}
public static String[] split(String str, int count){
String[] dat = new String[str.length() / count];
for(int i = 0; i < str.length(); i+=count){
dat[i / count] = str.substring(i, i + count);
}
return dat;
}
private static byte[] hex2bytes(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i + 1), 16));
}
return data;
}
public static String bytes2hex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
}
return new String(hexChars);
}
Here is my RawTransactionBuilder
class:
import java.math.BigDecimal;
import java.security.Signature;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.util.ArrayList;
import static com.xemplarsoft.Vars.TX_FEE;
import static com.xemplarsoft.libs.crypto.KeyManager.bytes2hex;
import static com.xemplarsoft.libs.crypto.KeyManager.getPubFromPriv;
import static com.xemplarsoft.libs.util.Sha256.hex2bytes;
public class RawTransactionBuilder {
public static final String VERSION = "01000000";
public static final String SEQUENCE_NUMBER = "ffffffff";
public static final String LOCKTIME = "00000000";
public final int inputCount, outputCount;
public final RawOutput[] outputs;
public final RawInput[] inputs;
public RawTransactionBuilder(ArrayList<UTXO> utxos, String from, String to, BigDecimal amount){
inputCount = utxos.size();
outputCount = 2;
outputs = new RawOutput[outputCount];
inputs = new RawInput[inputCount];
BigDecimal total = new BigDecimal(0);
for(int i = 0; i < utxos.size(); i++){
UTXO u = utxos.get(i);
total = total.add(u.getAmount());
inputs[i] = new RawInput(u.getTxId(), u.getScriptPubKey(), u.getVout());
}
BigDecimal change = total.subtract(amount).subtract(TX_FEE);
outputs[1] = new RawOutput(from, change);
outputs[0] = new RawOutput(to, amount);
}
public String buildUnsignedRawTX(){
long mills = System.currentTimeMillis();
String ret = VERSION; // VERSION
ret += hex2smallEndian(String.format("%1$08x", mills / 1000L)); // TIME
ret += hex2smallEndian(String.format("%1$02x", inputCount)); // INPUT COUNT
for(int i = 0; i < inputCount; i++){
RawInput in = inputs[i];
ret += in.getPrevTXHexLE(); // INPUT[i] PREV TXID
ret += in.getPrevOutIndexLE(); //INPUT[i] PREV OUTPUT INDEX
ret += "00"; //INPUT[i] SCRIPT LENGTH
ret += SEQUENCE_NUMBER; //INPUT[i] SEQUENCE NUMBER
}
ret += hex2smallEndian(String.format("%1$02x", outputCount)); // OUTPUT COUNT
for(int i = 0; i < outputCount; i++){
RawOutput out = outputs[i];
ret += out.getValueHexLE(); // OUTPUT[i] VALUE
ret += out.getScriptLengthHexLE(); //OUTPUT[i] PUBLIC KEY SCRIPT LENGTH
ret += out.getScriptPubKey(); //OUTPUT[i] PUBLIC KEY SCRIPT
}
return ret += LOCKTIME;
}
private String buildSignedRawTX(){
long mills = System.currentTimeMillis();
String ret = VERSION; // VERSION
ret += hex2smallEndian(String.format("%1$08x", mills / 1000L)); // TIME
ret += hex2smallEndian(String.format("%1$02x", inputCount)); // INPUT COUNT
for(int i = 0; i < inputCount; i++){
RawInput in = inputs[i];
ret += in.getPrevTXHexLE(); // INPUT[i] PREV TXID
ret += in.getPrevOutIndexLE(); //INPUT[i] PREV OUTPUT INDEX
ret += in.getScriptSigLength(); //INPUT[i] SCRIPT LENGTH
ret += in.getScriptSig(); //INPUT[i] SCRIPT SIG
ret += SEQUENCE_NUMBER; //INPUT[i] SEQUENCE NUMBER
}
ret += hex2smallEndian(String.format("%1$02x", outputCount)); // OUTPUT COUNT
for(int i = 0; i < outputCount; i++){
RawOutput out = outputs[i];
ret += out.getValueHexLE(); // OUTPUT[i] VALUE
ret += out.getScriptLengthHexLE(); //OUTPUT[i] PUBLIC KEY SCRIPT LENGTH
ret += out.getScriptPubKey(); //OUTPUT[i] PUBLIC KEY SCRIPT
}
return ret += LOCKTIME;
}
public String buildUnsignedRawTXForSiging(int inputIndex){
long mills = System.currentTimeMillis();
String ret = VERSION; // VERSION
ret += hex2smallEndian(String.format("%1$08x", mills / 1000L)); // TIME
ret += hex2smallEndian(String.format("%1$02x", inputCount)); // INPUT COUNT
for(int i = 0; i < inputCount; i++){
RawInput in = inputs[i];
ret += in.getPrevTXHexLE(); // INPUT[i] PREV TXID
ret += in.getPrevOutIndexLE(); //INPUT[i] PREV OUTPUT INDEX
if(i == inputIndex){
ret += in.getScriptPubKeyLength(); //INPUT[i] SCRIPT LENGTH
ret += in.getScriptPubKey(); //INPUT[i] TEMP SCRIPT SIG
} else {
ret += "00"; //INPUT[i] SCRIPT LENGTH
}
ret += SEQUENCE_NUMBER; //INPUT[i] SEQUENCE NUMBER
}
ret += hex2smallEndian(String.format("%1$02x", outputCount)); // OUTPUT COUNT
for(int i = 0; i < outputCount; i++){
RawOutput out = outputs[i];
ret += out.getValueHexLE(); // OUTPUT[i] VALUE
ret += out.getScriptLengthHexLE(); //OUTPUT[i] PUBLIC KEY SCRIPT LENGTH
ret += out.getScriptPubKey(); //OUTPUT[i] PUBLIC KEY SCRIPT
}
ret += LOCKTIME;
ret += "01000000";
return ret;
}
public String signTX(ECPrivateKey priv){
Signature sig = null;
ECPublicKey pub = null;
try{
pub = getPubFromPriv(priv);
for(int i = 0; i < inputs.length; i++) {
sig = Signature.getInstance("SHA256withECDSA");
sig.initSign(priv);
String hash = hex2smallEndian(bytes2hex(Sha256.getHash(Sha256.getHash(buildUnsignedRawTXForSiging(i)))));
sig.update(hex2bytes(hash));
inputs[i].setScriptSig(sig.sign(), pub);
}
} catch (Exception e){
e.printStackTrace();
return null;
}
return buildSignedRawTX();
}
My RawInput
class:
public class RawInput {
private final String prevTXID, scriptPubKey;
private final long prevOutIndex;
private String scriptSig;
public RawInput(String prevTX, String scriptPubKey, long prevOutIndex){
this.prevTXID = prevTX;
this.prevOutIndex = prevOutIndex;
this.scriptPubKey = scriptPubKey;
}
public String getPrevTXHexLE(){
return hex2smallEndian(prevTXID);
}
public String getPrevOutIndexLE(){
return hex2smallEndian(String.format("%1$08x", prevOutIndex));
}
public String getScriptPubKey(){
System.out.println("Script Pub Key: " + scriptPubKey);
return scriptPubKey;
}
public String getScriptPubKeyLength(){
return String.format("%1$02x", scriptPubKey.length() / 2);
}
public void setScriptSig(byte[] sig, ECPublicKey pub){
String signature = bytes2hex(sig) + "01";
ECPoint pt = pub.getW();
String sx = adjustTo64(pt.getAffineX().toString(16));
String sy = adjustTo64(pt.getAffineY().toString(16));
String pubkey = "1E" + sx + sy;
String sigLength = String.format("%1$02x", signature.length() / 2);
String pubLength = String.format("%1$02x", pubkey.length() / 2);
System.out.println("PUBKEY: " + convertPubToAddress(pubkey));
this.scriptSig = sigLength + signature + pubLength + pubkey;
}
public String getScriptSigLength(){
return String.format("%1$02x", scriptSig.length() / 2);
}
public String getScriptSig(){
return scriptSig;
}
}
And my payment method group that is called when a payment is requested:
//Payment Method Group
private String to, from, narration;
private BigDecimal amt, total;
public void makePayment(String to, BigDecimal amount, String narration, String from){
this.to = to;
this.from = from;
this.amt = amount;
this.total = amount.add(TX_FEE);
this.narration = narration;
cli.createRawTXList(from);
}
private void makePayment(ArrayList<UTXO> utxos){
if(utxos == null){
this.to = null;
this.from = null;
this.amt = null;
this.total = null;
this.narration = null;
return;
}
ArrayList<UTXO> required = new ArrayList<>();
ArrayList<UTXOverview> requiredOverview = new ArrayList<>();
BigDecimal soFar = new BigDecimal(0);
boolean needsMore = true;
int index = 0;
while(needsMore){
UTXO current = utxos.get(index);
soFar = soFar.add(current.getAmount());
required.add(current);
requiredOverview.add(current.asOverview());
if(soFar.compareTo(total) >= 0){
needsMore = false;
}
index++;
if(index >= utxos.size()) needsMore = false;
}
String WIF = "EXAMPLE_PRIV_KEY";
ECPrivateKey priv = convertWIFtoECPrivateKey(WIF);
System.out.println("WIF: " + convertPrivToWIF(adjustTo64(priv.getS().toString(16)).toUpperCase()));
RawTransactionBuilder raw = new RawTransactionBuilder(required, from, to, amt);
System.out.println("RAWTXBUILDER unsigned: " + raw.buildUnsignedRawTX());
System.out.println("RAWTXBUILDER txid: " + bytes2hex(Sha256.getHash(Sha256.getHash(raw.buildUnsignedRawTX()))));
try {
System.out.println("RAWTXBUILDER signed: " + raw.signTX(priv));
} catch (Exception e){
e.printStackTrace();
}
this.to = null;
this.amt = null;
this.total = null;
this.narration = null;
}
My problem is that whenever I run the above code, with sendrawtransaction
I get TX rejected (code -22)
which poins to an error of some sort. I've tried flipping the transaction hash to little endian, and leaving it as it generates. Signing the hash as a raw hex string and as raw bytes. I know that my convert
functions work as I have verified that the proper WIF and Address are recreated. I'm not sure where my issue is.