Skip to content

Instantly share code, notes, and snippets.

@daanporon
Last active January 5, 2019 11:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save daanporon/28087f042fb83507f5dc9389119a0508 to your computer and use it in GitHub Desktop.
Save daanporon/28087f042fb83507f5dc9389119a0508 to your computer and use it in GitHub Desktop.
Manual sign Multichain transaction
'use strict';
var crypto = require('crypto'),
secp256k1 = require('secp256k1');
var OPS = {
"OP_PUSHDATA1": 76,
"OP_PUSHDATA2": 77,
"OP_PUSHDATA4": 78,
"OP_DUP": 118,
"OP_HASH160": 169,
"OP_EQUALVERIFY": 136,
"OP_CHECKSIG": 172
};
var signTransaction = function(transaction, publicKey, privateKey) {
var ripemd160Hash = ripemd160(sha256(publicKey));
var chunks = [];
chunks.push(uint8Buffer(OPS.OP_DUP));
chunks.push(uint8Buffer(OPS.OP_HASH160));
chunks.push(pushDataIntBuffer(ripemd160Hash.length));
chunks.push(ripemd160Hash);
chunks.push(uint8Buffer(OPS.OP_EQUALVERIFY));
chunks.push(uint8Buffer(OPS.OP_CHECKSIG));
transaction.vin[0].script = Buffer.concat(chunks);
var hashType = 0x01; // SIGHASH_ALL
var hashForSignature = hash256(Buffer.concat([toBuffer(transaction), uint32Buffer(hashType)]));
console.log('hash for signature', hashForSignature.toString('hex'));
var signature = secp256k1.sign(hashForSignature, privateKey).signature;
var signatureDER = secp256k1.signatureExport(signature);
console.log('signature', signature.toString('hex'));
console.log('signature DER', signatureDER.toString('hex'));
var scriptSignature = Buffer.concat([signatureDER, uint8Buffer(hashType)]); // public key hash input
console.log('script signature', scriptSignature.toString('hex'));
var scriptSig = Buffer.concat([pushDataIntBuffer(scriptSignature.length), scriptSignature, pushDataIntBuffer(publicKey.length), publicKey]);
console.log('script sig', scriptSig.toString('hex'));
transaction.vin[0].script = scriptSig;
var signedTransaction = toBuffer(transaction);
console.log('signed hex string', signedTransaction.toString('hex'));
};
// hexstring: 0100000001a85d392dfbb9c0e3b91de2853d2ece6e42be30cbcd98319a6835b86ae0b08eea0100000000ffffffff0200000000000000003176a914c1fe3d898175f021f827fed320408adebc266a4288ac1673706b710700000008010000274701000000000000007500000000000000003176a91442ecb7a618d3fe7dd2c57d3a1d81a2433a4a4c7c88ac1673706b710700000008010000274762000000000000007500000000
signTransaction({
'txid': '6d0fa76f4862547a74e237af8461cfa8faaa3e17135f10a3643cc22c05ed70f5',
'version': 1,
'locktime': 0,
'vin': [
{
'txid': 'ea8eb0e06ab835689a3198cdcb30be426ece2e3d85e21db9e3c0b9fb2d395da8',
'vout': 1,
'scriptSig': {
'asm': '',
'hex': ''
},
'sequence': 4294967295
}
],
'vout': [
{
'value': 0,
'n': 0,
'scriptPubKey': {
'asm': 'OP_DUP OP_HASH160 c1fe3d898175f021f827fed320408adebc266a42 OP_EQUALVERIFY OP_CHECKSIG 73706b71070000000801000027470100000000000000 OP_DROP',
'hex': '76a914c1fe3d898175f021f827fed320408adebc266a4288ac1673706b7107000000080100002747010000000000000075',
'reqSigs': 1,
'type': 'pubkeyhash',
'addresses': [
'1TDi2mfj5A73p3ME6x7nCJSuC4tAPajXV6tJBn'
]
},
'assets': [
{
'name': 'VOTE',
'issuetxid': '2747d7ffebcdae0638e9afed48371df16fbdfadc5174f4cef459d36d9354d14a',
'assetref': '7-264-18215',
'qty': 1,
'raw': 1
}
],
'permissions': []
},
{
'value': 0,
'n': 1,
'scriptPubKey': {
'asm': 'OP_DUP OP_HASH160 42ecb7a618d3fe7dd2c57d3a1d81a2433a4a4c7c OP_EQUALVERIFY OP_CHECKSIG 73706b71070000000801000027476200000000000000 OP_DROP',
'hex': '76a91442ecb7a618d3fe7dd2c57d3a1d81a2433a4a4c7c88ac1673706b7107000000080100002747620000000000000075',
'reqSigs': 1,
'type': 'pubkeyhash',
'addresses': [
'1A3d1nw4eCTgaxGnZ6YFZbt4yC7u8TJPPof3dc'
]
},
'assets': [
{
'name': 'VOTE',
'issuetxid': '2747d7ffebcdae0638e9afed48371df16fbdfadc5174f4cef459d36d9354d14a',
'assetref': '7-264-18215',
'qty': 98,
'raw': 98
}
],
'permissions': []
}
],
'data': []
}, Buffer.from('02fdcee0919d98a318fb34318ab6645420252eadf5cbfe09f5c4cc07d58f22c628', 'hex'), Buffer.from('825119fcc0c096916d192a8b62f69a78996cc3c7a73fe8665b74b7d7fe6d9a98', 'hex'));
// result:
// 0100000001a85d392dfbb9c0e3b91de2853d2ece6e42be30cbcd98319a6835b86ae0b08eea010000006b483045022100c424fe2e42e9223ca5ddbfa6dc7fe4964997d16aa53910924aeca0a2330c87e002207710aaeb20845f8d08b78295b05f5c90c3cb96025d893f8c53345d677a1def18012102fdcee0919d98a318fb34318ab6645420252eadf5cbfe09f5c4cc07d58f22c628ffffffff0200000000000000003176a914c1fe3d898175f021f827fed320408adebc266a4288ac1673706b710700000008010000274701000000000000007500000000000000003176a91442ecb7a618d3fe7dd2c57d3a1d81a2433a4a4c7c88ac1673706b710700000008010000274762000000000000007500000000
// result RPC call:
// 0100000001a85d392dfbb9c0e3b91de2853d2ece6e42be30cbcd98319a6835b86ae0b08eea010000006a473044022032e6bffb014c0929d1aa0fb2a0eeeb37858ffe9bc44697f9a51fce2b41cb5695022025d72a36fd104d4a8666f3248d06b7456d3acd98be08de913577a6e25ae539e5012102fdcee0919d98a318fb34318ab6645420252eadf5cbfe09f5c4cc07d58f22c628ffffffff0200000000000000003176a914c1fe3d898175f021f827fed320408adebc266a4288ac1673706b710700000008010000274701000000000000007500000000000000003176a91442ecb7a618d3fe7dd2c57d3a1d81a2433a4a4c7c88ac1673706b710700000008010000274762000000000000007500000000
function toBuffer(transaction) {
var chunks = [];
chunks.push(uint32Buffer(transaction.version));
chunks.push(varIntBuffer(transaction.vin.length));
transaction.vin.forEach(function (txIn) {
var hash = [].reverse.call(new Buffer(txIn.txid, 'hex'));
chunks.push(hash);
chunks.push(uint32Buffer(txIn.vout)); // index
if (txIn.script != null) {
chunks.push(varIntBuffer(txIn.script.length));
chunks.push(txIn.script);
} else {
chunks.push(varIntBuffer(0));
}
chunks.push(uint32Buffer(txIn.sequence));
});
chunks.push(varIntBuffer(transaction.vout.length));
transaction.vout.forEach(function (txOut) {
chunks.push(uint64Buffer(txOut.value));
var script = Buffer.from(txOut.scriptPubKey.hex, 'hex');
chunks.push(varIntBuffer(script.length));
chunks.push(script);
});
chunks.push(uint32Buffer(transaction.locktime));
return Buffer.concat(chunks);
}
function pushDataIntBuffer(number) {
var chunks = [];
var pushDataSize = number < OPS.OP_PUSHDATA1 ? 1
: number < 0xff ? 2
: number < 0xffff ? 3
: 5;
if (pushDataSize === 1) {
chunks.push(uint8Buffer(number));
} else if (pushDataSize === 2) {
chunks.push(uint8Buffer(OPS.OP_PUSHDATA1));
chunks.push(uint8Buffer(number));
} else if (pushDataSize === 3) {
chunks.push(uint8Buffer(OPS.OP_PUSHDATA2));
chunks.push(uint16Buffer(number));
} else {
chunks.push(uint8Buffer(OPS.OP_PUSHDATA4));
chunks.push(uint32Buffer(number));
}
return Buffer.concat(chunks);
}
function varIntBuffer(number) {
var chunks = [];
var size = number < 253 ? 1
: number < 0x10000 ? 3
: number < 0x100000000 ? 5
: 9;
// 8 bit
if (size === 1) {
chunks.push(uint8Buffer(number));
// 16 bit
} else if (size === 3) {
chunks.push(uint8Buffer(253));
chunks.push(uint16Buffer(number));
// 32 bit
} else if (size === 5) {
chunks.push(uint8Buffer(254));
chunks.push(uint32Buffer(number));
// 64 bit
} else {
chunks.push(uint8Buffer(255));
chunks.push(uint64Buffer(number));
}
return Buffer.concat(chunks);
}
function uint8Buffer(number) {
var buffer = new Buffer(1);
buffer.writeUInt8(number, 0);
return buffer;
}
function uint16Buffer(number) {
var buffer = new Buffer(2);
buffer.writeUInt16LE(number, 0);
return buffer;
}
function uint32Buffer(number) {
var buffer = new Buffer(4);
buffer.writeUInt32LE(number, 0);
return buffer;
}
function uint64Buffer(number) {
var buffer = new Buffer(8);
buffer.writeInt32LE(number & -1, 0);
buffer.writeUInt32LE(Math.floor(number / 0x100000000), 4);
return buffer;
}
function hash256 (buffer) {
return sha256(sha256(buffer))
}
function ripemd160 (buffer) {
return crypto.createHash('rmd160').update(buffer).digest()
}
function sha256 (buffer) {
return crypto.createHash('sha256').update(buffer).digest()
}
@daanporon
Copy link
Author

result:
0100000001a85d392dfbb9c0e3b91de2853d2ece6e42be30cbcd98319a6835b86ae0b08eea010000006b483045022100c424fe2e42e9223ca5ddbfa6dc7fe4964997d16aa53910924aeca0a2330c87e002207710aaeb20845f8d08b78295b05f5c90c3cb96025d893f8c53345d677a1def18012102fdcee0919d98a318fb34318ab6645420252eadf5cbfe09f5c4cc07d58f22c628ffffffff0200000000000000003176a914c1fe3d898175f021f827fed320408adebc266a4288ac1673706b710700000008010000274701000000000000007500000000000000003176a91442ecb7a618d3fe7dd2c57d3a1d81a2433a4a4c7c88ac1673706b710700000008010000274762000000000000007500000000

result from RPC call:
0100000001a85d392dfbb9c0e3b91de2853d2ece6e42be30cbcd98319a6835b86ae0b08eea010000006a473044022032e6bffb014c0929d1aa0fb2a0eeeb37858ffe9bc44697f9a51fce2b41cb5695022025d72a36fd104d4a8666f3248d06b7456d3acd98be08de913577a6e25ae539e5012102fdcee0919d98a318fb34318ab6645420252eadf5cbfe09f5c4cc07d58f22c628ffffffff0200000000000000003176a914c1fe3d898175f021f827fed320408adebc266a4288ac1673706b710700000008010000274701000000000000007500000000000000003176a91442ecb7a618d3fe7dd2c57d3a1d81a2433a4a4c7c88ac1673706b710700000008010000274762000000000000007500000000

@romstyop
Copy link

romstyop commented May 4, 2017

Hi, daanporon!
Thank you very much for your work!
I'm trying to use the code above in the web browser and the result differs from the result from RPC call.
When I compare results, see difference in signature (signature DES).
I think the problem is in the transmission of private key to the signTransaction function.
I have private key in wif format and use bitcoin-js library for manage it.
All my actions step by step i put below.

Can you explain to me what am I doing wrong?
Thanks.

multichain-cli wallets createrawsendfrom 1D5y7FeVySiwFGaDGypJbB9WNNkUiHgCwW "{"145MCTHfKu8fFzVFvAEkD8cv2DNB4xnybc":{"openAsset":3}}" "[]"

0100000001c35312d70472624d78d3c18b7c2012b49d70e3256ec2eed8bbc45f5ffb37aa860000000000ffffffff0200000000000000003776a91421bae6b4d78b63f5691fb920f0ea1e12dea30e8f88ac1c73706b71be8f53808eea929b60b1c9d647cb6dbc2c010000000000007500000000000000003776a9148492027ada4f94978b86ffe7f1a707bb09d2df1b88ac1c73706b71be8f53808eea929b60b1c9d647cb6dbcc8000000000000007500000000

multichain-cli wallets decoderawtransaction 0100000001c35312d70472624d78d3c18b7c2012b49d70e3256ec2eed8bbc45f5ffb37aa860000000000ffffffff0200000000000000003776a91421bae6b4d78b63f5691fb920f0ea1e12dea30e8f88ac1c73706b71be8f53808eea929b60b1c9d647cb6dbc2c010000000000007500000000000000003776a9148492027ada4f94978b86ffe7f1a707bb09d2df1b88ac1c73706b71be8f53808eea929b60b1c9d647cb6dbcc8000000000000007500000000

{
"txid" : "a07a8c447a08674d9dc4417ec56097144e277327a633f55ce6fbc81b7c59ea51",
"version" : 1,
"locktime" : 0,
"vin" : [
{
"txid" : "86aa37fb5f5fc4bbd8eec26e25e3709db412207c8bc1d3784d627204d71253c3",
"vout" : 0,
"scriptSig" : {
"asm" : "",
"hex" : ""
},
"sequence" : 4294967295
}
],
"vout" : [
{
"value" : 0.00000000,
"n" : 0,
"scriptPubKey" : {
"asm" : "OP_DUP OP_HASH160 21bae6b4d78b63f5691fb920f0ea1e12dea30e8f OP_EQUALVERIFY OP_CHECKSIG 73706b71be8f53808eea929b60b1c9d647cb6dbc2c01000000000000 OP_DROP",
"hex" : "76a91421bae6b4d78b63f5691fb920f0ea1e12dea30e8f88ac1c73706b71be8f53808eea929b60b1c9d647cb6dbc2c0100000000000075",
"reqSigs" : 1,
"type" : "pubkeyhash",
"addresses" : [
"145MCTHfKu8fFzVFvAEkD8cv2DNB4xnybc"
]
},
"assets" : [
{
"name" : "openAsset",
"issuetxid" : "bc6dcb47d6c9b1609b92ea8e80538fbe47664a8dd9b8f041c63ac37206030b5f",
"assetref" : "60-265-28092",
"qty" : 3.00000000,
"raw" : 300,
"type" : "transfer"
}
],
"permissions" : [
],
"items" : [
]
},
{
"value" : 0.00000000,
"n" : 1,
"scriptPubKey" : {
"asm" : "OP_DUP OP_HASH160 8492027ada4f94978b86ffe7f1a707bb09d2df1b OP_EQUALVERIFY OP_CHECKSIG 73706b71be8f53808eea929b60b1c9d647cb6dbcc800000000000000 OP_DROP",
"hex" : "76a9148492027ada4f94978b86ffe7f1a707bb09d2df1b88ac1c73706b71be8f53808eea929b60b1c9d647cb6dbcc80000000000000075",
"reqSigs" : 1,
"type" : "pubkeyhash",
"addresses" : [
"1D5y7FeVySiwFGaDGypJbB9WNNkUiHgCwW"
]
},
"assets" : [
{
"name" : "openAsset",
"issuetxid" : "bc6dcb47d6c9b1609b92ea8e80538fbe47664a8dd9b8f041c63ac37206030b5f",
"assetref" : "60-265-28092",
"qty" : 2.00000000,
"raw" : 200,
"type" : "transfer"
}
],
"permissions" : [
],
"items" : [
]
}
],
"data" : [
]
}

multichain-cli wallets signrawtransaction 0100000001c35312d70472624d78d3c18b7c2012b49d70e3256ec2eed8bbc45f5ffb37aa860000000000ffffffff0200000000000000003776a91421bae6b4d78b63f5691fb920f0ea1e12dea30e8f88ac1c73706b71be8f53808eea929b60b1c9d647cb6dbc2c010000000000007500000000000000003776a9148492027ada4f94978b86ffe7f1a707bb09d2df1b88ac1c73706b71be8f53808eea929b60b1c9d647cb6dbcc8000000000000007500000000 "[]" "["KyTmUpr9JoeftiXKz6DoPUEk7g3ePPCeXU7pXLtFGEzbcrFj6SGy"]"

{
"hex" : "0100000001c35312d70472624d78d3c18b7c2012b49d70e3256ec2eed8bbc45f5ffb37aa86000000006a473044022074fe405664f9b7481d9203c7ac0a0d3a862aa7d388b1a47161478602e0fc6ea2022061a9da26cf8273a629506182b1dfac5cd99d0d6c7a93d29557ac5fddda699798012102d13bdbb64759c69c71e5fb3a7794209ef44b6cffb56904121e24747476dc985cffffffff0200000000000000003776a91421bae6b4d78b63f5691fb920f0ea1e12dea30e8f88ac1c73706b71be8f53808eea929b60b1c9d647cb6dbc2c010000000000007500000000000000003776a9148492027ada4f94978b86ffe7f1a707bb09d2df1b88ac1c73706b71be8f53808eea929b60b1c9d647cb6dbcc8000000000000007500000000",
"complete" : true
}

from sign.js
var keyPair = bitcoin.ECPair.fromWIF("KyTmUpr9JoeftiXKz6DoPUEk7g3ePPCeXU7pXLtFGEzbcrFj6SGy", bitcoin.networks.bitcoin);

signTransaction(jsonTransaction, Buffer.from(keyPair.getPublicKeyBuffer().toString('hex'), 'hex'), Buffer.from(keyPair.d.toHex(), 'hex'));

0100000001c35312d70472624d78d3c18b7c2012b49d70e3256ec2eed8bbc45f5ffb37aa86000000006a4730440220215ddd43ac8915fa3d6e93956161c0d7997a12a292ee80e2a4b14e0cd635f5c402207a9cab5c8f010b262929aea3f0f1e91a091ed2e893c6a2214ebd55fe06ea84d9012102d13bdbb64759c69c71e5fb3a7794209ef44b6cffb56904121e24747476dc985cffffffff0200000000000000003776a91421bae6b4d78b63f5691fb920f0ea1e12dea30e8f88ac1c73706b71be8f53808eea929b60b1c9d647cb6dbc2c010000000000007500000000000000003776a9148492027ada4f94978b86ffe7f1a707bb09d2df1b88ac1c73706b71be8f53808eea929b60b1c9d647cb6dbcc8000000000000007500000000

@phantanthanh1996
Copy link

Hi, i am using your function but i get a error after sendrawtransaction
error code: -26
error message:
16: ConnectInputs failed: mandatory-script-verify-flag-failed (Script failed an OP_CHECKSIGVERIFY operation)

@roytang
Copy link

roytang commented Aug 31, 2018

Hi Daan,

Would you happen to know how to modify your function to sign multiple inputs? I assumed the approach would be similar to https://bitcoin.stackexchange.com/questions/41209/how-to-sign-a-transaction-with-multiple-inputs, where I just need to clear the inputs for each other vin to generate the hash to be signed for each input:

` var signTransaction = function(transaction, publicKey, privateKey) {
var ripemd160Hash = ripemd160(sha256(publicKey));

    var chunks = [];
    chunks.push(uint8Buffer(OPS.OP_DUP));
    chunks.push(uint8Buffer(OPS.OP_HASH160));
    chunks.push(pushDataIntBuffer(ripemd160Hash.length));
    chunks.push(ripemd160Hash);
    chunks.push(uint8Buffer(OPS.OP_EQUALVERIFY));
    chunks.push(uint8Buffer(OPS.OP_CHECKSIG));

    let newScripts = [];
    for (let index = 0, size=transaction.vin.length; index<size; index++) {
        for (let i = 0; i<size; i++) {
          delete transaction.vin[i].script;
        }
        transaction.vin[index].script = Buffer.concat(chunks);

        console.log("checking..." + index);
        for (let index2 = 0; index2<size; index2++) {
          console.log('script ' + index2, transaction.vin[index2].script);
        }
      
        var hashType = 0x01;  // SIGHASH_ALL

        var hashForSignature = hash256(Buffer.concat([toBuffer(transaction), uint32Buffer(hashType)]));
    
        console.log('hash for signature', hashForSignature.toString('hex'));
    
        var signature = secp256k1.sign(hashForSignature, privateKey).signature;
        var signatureDER = secp256k1.signatureExport(signature);
    
        console.log('signature', signature.toString('hex'));
        console.log('signature DER', signatureDER.toString('hex'));
    
        var scriptSignature = Buffer.concat([signatureDER, uint8Buffer(hashType)]); // public key hash input
    
        console.log('script signature', scriptSignature.toString('hex'));
    
        var scriptSig = Buffer.concat([pushDataIntBuffer(scriptSignature.length), scriptSignature, pushDataIntBuffer(publicKey.length), publicKey]);
    
        console.log('script sig', scriptSig.toString('hex'));
    
        newScripts[index] = scriptSig;
    }
    for (let index = 0, size=transaction.vin.length; index<size; index++) {
      transaction.vin[index].script = newScripts[index];
    }

    console.log(transaction);

    var signedTransaction = toBuffer(transaction);

    console.log('signed transaction', signedTransaction);

    console.log('signed hex string', signedTransaction.toString('hex'));

    return signedTransaction.toString('hex');
};

`

But when I tested this with a transaction where I had to sign two inputs (using the same private key), I found that the generated signature for the first vin is incorrect, but the second vin's is correct (as compared to when signing using multichain's signrawtransaction). So I figure I'm at least on the right track and maybe just missing a step or two somewhere, would you have any advice?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment