Merge pull request #8 from sairajzero/master

Multisig utils and fixes
This commit is contained in:
Sai Raj 2023-02-25 15:18:38 +05:30 committed by GitHub
commit 17ee486948
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 349 additions and 45 deletions

View File

@ -1,4 +1,4 @@
(function (EXPORTS) { //floBlockchainAPI v2.4.0
(function (EXPORTS) { //floBlockchainAPI v2.4.3
/* FLO Blockchain Operator to send/receive data from blockchain using API calls*/
'use strict';
const floBlockchainAPI = EXPORTS;
@ -15,6 +15,13 @@
receiverID: floGlobals.adminID
};
const SATOSHI_IN_BTC = 1e8;
const util = floBlockchainAPI.util = {};
util.Sat_to_FLO = value => parseFloat((value / SATOSHI_IN_BTC).toFixed(8));
util.FLO_to_Sat = value => parseInt(value * SATOSHI_IN_BTC);
Object.defineProperties(floBlockchainAPI, {
sendAmt: {
get: () => DEFAULT.sendAmt,
@ -121,8 +128,8 @@
});
}
//Send Tx to blockchain
const sendTx = floBlockchainAPI.sendTx = function (senderAddr, receiverAddr, sendAmt, privKey, floData = '', strict_utxo = true) {
//create a transaction with single sender
const createTx = function (senderAddr, receiverAddr, sendAmt, floData = '', strict_utxo = true) {
return new Promise((resolve, reject) => {
if (!floCrypto.validateASCII(floData))
return reject("Invalid FLO_Data: only printable ASCII characters are allowed");
@ -130,8 +137,6 @@
return reject(`Invalid address : ${senderAddr}`);
else if (!floCrypto.validateFloID(receiverAddr))
return reject(`Invalid address : ${receiverAddr}`);
else if (privKey.length < 1 || !floCrypto.verifyPrivKey(privKey, senderAddr))
return reject("Invalid Private key!");
else if (typeof sendAmt !== 'number' || sendAmt <= 0)
return reject(`Invalid sendAmt : ${sendAmt}`);
@ -175,15 +180,36 @@
if (change > DEFAULT.minChangeAmt)
trx.addoutput(senderAddr, change);
trx.addflodata(floData.replace(/\n/g, ' '));
var signedTxHash = trx.sign(privKey, 1);
broadcastTx(signedTxHash)
.then(txid => resolve(txid))
.catch(error => reject(error))
resolve(trx);
}
}).catch(error => reject(error))
}).catch(error => reject(error))
}).catch(error => reject(error))
}).catch(error => reject(error))
})
}
floBlockchainAPI.createTx = function (senderAddr, receiverAddr, sendAmt, floData = '', strict_utxo = true) {
return new Promise((resolve, reject) => {
createTx(senderAddr, receiverAddr, sendAmt, floData, strict_utxo)
.then(trx => resolve(trx.serialize()))
.catch(error => reject(error))
})
}
//Send Tx to blockchain
const sendTx = floBlockchainAPI.sendTx = function (senderAddr, receiverAddr, sendAmt, privKey, floData = '', strict_utxo = true) {
return new Promise((resolve, reject) => {
if (!floCrypto.validateFloID(senderAddr, true))
return reject(`Invalid address : ${senderAddr}`);
else if (privKey.length < 1 || !floCrypto.verifyPrivKey(privKey, senderAddr))
return reject("Invalid Private key!");
createTx(senderAddr, receiverAddr, sendAmt, floData, strict_utxo).then(trx => {
var signedTxHash = trx.sign(privKey, 1);
broadcastTx(signedTxHash)
.then(txid => resolve(txid))
.catch(error => reject(error))
}).catch(error => reject(error))
});
}
@ -419,7 +445,7 @@
}
//Create a multisig transaction
const createMultisigTx = floBlockchainAPI.createMultisigTx = function (redeemScript, receivers, amounts, floData = '', strict_utxo = true) {
const createMultisigTx = function (redeemScript, receivers, amounts, floData = '', strict_utxo = true) {
return new Promise((resolve, reject) => {
var multisig = floCrypto.decodeRedeemScript(redeemScript);
@ -499,6 +525,15 @@
});
}
//Same as above, but explict call should return serialized tx-hex
floBlockchainAPI.createMultisigTx = function (redeemScript, receivers, amounts, floData = '', strict_utxo = true) {
return new Promise((resolve, reject) => {
createMultisigTx(redeemScript, receivers, amounts, floData, strict_utxo)
.then(trx => resolve(trx.serialize()))
.catch(error => reject(error))
})
}
//Create and send multisig transaction
const sendMultisigTx = floBlockchainAPI.sendMultisigTx = function (redeemScript, privateKeys, receivers, amounts, floData = '', strict_utxo = true) {
return new Promise((resolve, reject) => {
@ -538,6 +573,144 @@
})
}
function deserializeTx(tx) {
if (typeof tx === 'string' || Array.isArray(tx)) {
try {
tx = bitjs.transaction(tx);
} catch {
throw "Invalid transaction hex";
}
} else if (typeof tx !== 'object' || typeof tx.sign !== 'function')
throw "Invalid transaction object";
return tx;
}
floBlockchainAPI.signTx = function (tx, privateKey, sighashtype = 1) {
if (!floCrypto.getFloID(privateKey))
throw "Invalid Private key";
//deserialize if needed
tx = deserializeTx(tx);
var signedTxHex = tx.sign(privateKey, sighashtype);
return signedTxHex;
}
const checkSigned = floBlockchainAPI.checkSigned = function (tx, bool = true) {
tx = deserializeTx(tx);
let n = [];
for (let i = 0; i < tx.inputs.length; i++) {
var s = tx.scriptDecode(i);
if (s['type'] === 'scriptpubkey')
n.push(s.signed);
else if (s['type'] === 'multisig') {
var rs = tx.decodeRedeemScript(s['rs']);
let x = {
s: 0,
r: rs['required'],
t: rs['pubkeys'].length
};
//check input script for signatures
var script = Array.from(tx.inputs[i].script);
if (script[0] == 0) { //script with signatures
script = tx.parseScript(script);
for (var k = 0; k < script.length; k++)
if (Array.isArray(script[k]) && script[k][0] == 48) //0x30 DERSequence
x.s++;
}
//validate counts
if (x.r > x.t)
throw "signaturesRequired is more than publicKeys";
else if (x.s < x.r)
n.push(x);
else
n.push(true);
}
}
return bool ? !(n.filter(x => x !== true).length) : n;
}
floBlockchainAPI.checkIfSameTx = function (tx1, tx2) {
tx1 = deserializeTx(tx1);
tx2 = deserializeTx(tx2);
//compare input and output length
if (tx1.inputs.length !== tx2.inputs.length || tx1.outputs.length !== tx2.outputs.length)
return false;
//compare flodata
if (tx1.floData !== tx2.floData)
return false
//compare inputs
for (let i = 0; i < tx1.inputs.length; i++)
if (tx1.inputs[i].outpoint.hash !== tx2.inputs[i].outpoint.hash || tx1.inputs[i].outpoint.index !== tx2.inputs[i].outpoint.index)
return false;
//compare outputs
for (let i = 0; i < tx1.outputs.length; i++)
if (tx1.outputs[i].value !== tx2.outputs[i].value || Crypto.util.bytesToHex(tx1.outputs[i].script) !== Crypto.util.bytesToHex(tx2.outputs[i].script))
return false;
return true;
}
floBlockchainAPI.transactionID = function (tx) {
tx = deserializeTx(tx);
let clone = bitjs.clone(tx);
let raw_bytes = Crypto.util.hexToBytes(clone.serialize());
let txid = Crypto.SHA256(Crypto.SHA256(raw_bytes, { asBytes: true }), { asBytes: true }).reverse();
return Crypto.util.bytesToHex(txid);
}
const getTxOutput = (txid, i) => new Promise((resolve, reject) => {
fetch_api(`api/tx/${txid}`)
.then(result => resolve(result.vout[i]))
.catch(error => reject(error))
});
function getOutputAddress(outscript) {
var bytes, version;
switch (outscript[0]) {
case 118: //legacy
bytes = outscript.slice(3, outscript.length - 2);
version = bitjs.pub;
break
case 169: //multisig
bytes = outscript.slice(2, outscript.length - 1);
version = bitjs.multisig;
break;
default: return; //unknown
}
bytes.unshift(version);
var hash = Crypto.SHA256(Crypto.SHA256(bytes, { asBytes: true }), { asBytes: true });
var checksum = hash.slice(0, 4);
return bitjs.Base58.encode(bytes.concat(checksum));
}
floBlockchainAPI.parseTransaction = function (tx) {
return new Promise((resolve, reject) => {
tx = deserializeTx(tx);
let result = {};
let promises = [];
//Parse Inputs
for (let i = 0; i < tx.inputs.length; i++)
promises.push(getTxOutput(tx.inputs[i].outpoint.hash, tx.inputs[i].outpoint.index));
Promise.all(promises).then(inputs => {
result.inputs = inputs.map(inp => Object({
address: inp.scriptPubKey.addresses[0],
value: parseFloat(inp.value)
}));
let signed = checkSigned(tx, false);
result.inputs.forEach((inp, i) => inp.signed = signed[i]);
//Parse Outputs
result.outputs = tx.outputs.map(out => Object({
address: getOutputAddress(out.script),
value: util.Sat_to_FLO(out.value)
}))
//Parse Totals
result.total_input = parseFloat(result.inputs.reduce((a, inp) => a += inp.value, 0).toFixed(8));
result.total_output = parseFloat(result.outputs.reduce((a, out) => a += out.value, 0).toFixed(8));
result.fee = parseFloat((result.total_input - result.total_output).toFixed(8));
result.floData = tx.floData;
resolve(result);
}).catch(error => reject(error))
})
}
//Broadcast signed Tx in blockchain using API
const broadcastTx = floBlockchainAPI.broadcastTx = function (signedTxHash) {
return new Promise((resolve, reject) => {

View File

@ -1,4 +1,4 @@
(function (EXPORTS) { //floCrypto v2.3.4a
(function (EXPORTS) { //floCrypto v2.3.5a
/* FLO Crypto Operators */
'use strict';
const floCrypto = EXPORTS;
@ -234,7 +234,7 @@
floCrypto.getMultisigAddress = function (publicKeyList, requiredSignatures) {
if (!Array.isArray(publicKeyList) || !publicKeyList.length)
return null;
if (!Number.isInteger(requiredSignatures) || requiredSignatures < 1)
if (!Number.isInteger(requiredSignatures) || requiredSignatures < 1 || requiredSignatures > publicKeyList.length)
return null;
try {
var multisig = bitjs.pubkeys2multisig(publicKeyList, requiredSignatures);
@ -290,13 +290,15 @@
return false;
}
//Check the public-key for the address (any blockchain)
//Check the public-key (or redeem-script) for the address (any blockchain)
floCrypto.verifyPubKey = function (pubKeyHex, address) {
let raw = decodeAddress(address),
pub_hash = Crypto.util.bytesToHex(ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(pubKeyHex), {
asBytes: true
})));
return raw ? pub_hash === raw.hex : false;
let raw = decodeAddress(address);
if (!raw)
return;
let pub_hash = Crypto.util.bytesToHex(ripemd160(Crypto.SHA256(Crypto.util.hexToBytes(pubKeyHex), { asBytes: true })));
if (typeof raw.bech_version !== 'undefined' && raw.bytes.length == 32) //bech32-multisig
raw.hex = Crypto.util.bytesToHex(ripemd160(raw.bytes, { asBytes: true }));
return pub_hash === raw.hex;
}
//Convert the given address (any blockchain) to equivalent floID
@ -306,7 +308,7 @@
let raw = decodeAddress(address);
if (!raw)
return;
else if (options) {
else if (options) { //if (optional) version check is passed
if (typeof raw.version !== 'undefined' && (!options.std || !options.std.includes(raw.version)))
return;
if (typeof raw.bech_version !== 'undefined' && (!options.bech || !options.bech.includes(raw.bech_version)))
@ -321,6 +323,35 @@
return bitjs.Base58.encode(raw.bytes.concat(hash.slice(0, 4)));
}
//Convert the given multisig address (any blockchain) to equivalent multisig floID
floCrypto.toMultisigFloID = function (address, options = null) {
if (!address)
return;
let raw = decodeAddress(address);
if (!raw)
return;
else if (options) { //if (optional) version check is passed
if (typeof raw.version !== 'undefined' && (!options.std || !options.std.includes(raw.version)))
return;
if (typeof raw.bech_version !== 'undefined' && (!options.bech || !options.bech.includes(raw.bech_version)))
return;
}
if (typeof raw.bech_version !== 'undefined') {
if (raw.bytes.length != 32) return; //multisig bech address have 32 bytes
//multisig-bech:hash=SHA256 whereas multisig:hash=r160(SHA265), thus ripemd160 the bytes from multisig-bech
raw.bytes = ripemd160(raw.bytes, {
asBytes: true
});
}
raw.bytes.unshift(bitjs.multisig);
let hash = Crypto.SHA256(Crypto.SHA256(raw.bytes, {
asBytes: true
}), {
asBytes: true
});
return bitjs.Base58.encode(raw.bytes.concat(hash.slice(0, 4)));
}
//Checks if the given addresses (any blockchain) are same (w.r.t keys)
floCrypto.isSameAddr = function (addr1, addr2) {
if (!addr1 || !addr2)
@ -329,8 +360,13 @@
raw2 = decodeAddress(addr2);
if (!raw1 || !raw2)
return false;
else
else {
if (typeof raw1.bech_version !== 'undefined' && raw1.bytes.length == 32) //bech32-multisig
raw1.hex = Crypto.util.bytesToHex(ripemd160(raw1.bytes, { asBytes: true }));
if (typeof raw2.bech_version !== 'undefined' && raw2.bytes.length == 32) //bech32-multisig
raw2.hex = Crypto.util.bytesToHex(ripemd160(raw2.bytes, { asBytes: true }));
return raw1.hex === raw2.hex;
}
}
const decodeAddress = floCrypto.decodeAddr = function (address) {
@ -350,7 +386,7 @@
hex: Crypto.util.bytesToHex(bytes),
bytes
}
} else if (address.length == 42) { //bech encoding
} else if (address.length == 42 || address.length == 62) { //bech encoding
let decode = coinjs.bech32_decode(address);
if (decode) {
let bytes = decode.data;

View File

@ -1,4 +1,4 @@
(function (GLOBAL) { //lib v1.4.1a
(function (GLOBAL) { //lib v1.4.2b
'use strict';
/* Utility Libraries required for Standard operations
* All credits for these codes belong to their respective creators, moderators and owners.
@ -4355,6 +4355,12 @@
bitjs.multisig = 0x5e; //flochange - prefix for FLO Mainnet Multisig 0x5e
bitjs.compressed = false;
if (GLOBAL.cryptocoin == 'FLO_TEST') {
bitjs.pub = 0x73; // flochange - changed the prefix to FLO TestNet PublicKey Prefix 0x73
bitjs.priv = 0xa3; //flochange - changed the prefix to FLO TestNet Private key prefix 0xa3
bitjs.multisig = 0xc6; //flochange - prefix for FLO TestNet Multisig 0xc6
}
/* provide a privkey and return an WIF */
bitjs.privkey2wif = function (h) {
var r = Crypto.util.hexToBytes(h);
@ -4492,7 +4498,7 @@
};
}
bitjs.transaction = function () {
bitjs.transaction = function (tx_data = undefined) {
var btrx = {};
btrx.version = 2; //flochange look at this version
btrx.inputs = [];
@ -4521,14 +4527,12 @@
if (addr.version === bitjs.pub) { // regular address
buf.push(118); //OP_DUP
buf.push(169); //OP_HASH160
buf.push(addr.bytes.length);
buf = buf.concat(addr.bytes); // address in bytes
buf = this.writeBytesToScriptBuffer(buf, addr.bytes);// address in bytes
buf.push(136); //OP_EQUALVERIFY
buf.push(172); //OP_CHECKSIG
} else if (addr.version === bitjs.multisig) { // multisig address
buf.push(169); //OP_HASH160
buf.push(addr.bytes.length);
buf = buf.concat(addr.bytes); // address in bytes
buf = this.writeBytesToScriptBuffer(buf, addr.bytes);// address in bytes
buf.push(135); //OP_EQUAL
}
@ -4790,6 +4794,27 @@
return KBigInt;
};
btrx.writeBytesToScriptBuffer = function (buf, bytes) {
if (bytes.length < 76) { //OP_PUSHDATA1
buf.push(bytes.length);
} else if (bytes.length <= 0xff) {
buf.push(76); //OP_PUSHDATA1
buf.push(bytes.length);
} else if (bytes.length <= 0xffff) {
buf.push(77); //OP_PUSHDATA2
buf.push(bytes.length & 0xff);
buf.push((bytes.length >>> 8) & 0xff);
} else {
buf.push(78); //OP_PUSHDATA4
buf.push(bytes.length & 0xff);
buf.push((bytes.length >>> 8) & 0xff);
buf.push((bytes.length >>> 16) & 0xff);
buf.push((bytes.length >>> 24) & 0xff);
}
buf = buf.concat(bytes);
return buf;
}
btrx.parseScript = function (script) {
var chunks = [];
@ -4853,8 +4878,7 @@
var signature = this.transactionSig(index, wif, shType);
var buf = [];
var sigBytes = Crypto.util.hexToBytes(signature);
buf.push(sigBytes.length);
buf = buf.concat(sigBytes);
buf = this.writeBytesToScriptBuffer(buf, sigBytes);
var pubKeyBytes = Crypto.util.hexToBytes(key['pubkey']);
buf.push(pubKeyBytes.length);
buf = buf.concat(pubKeyBytes);
@ -4902,16 +4926,14 @@
for (let y in sigsList) {
var sighash = Crypto.util.hexToBytes(this.transactionHash(index, sigsList[y].slice(-1)[0] * 1));
if (bitjs.verifySignature(sighash, sigsList[y], pubkeyList[x])) {
buf.push(sigsList[y].length);
buf = buf.concat(sigsList[y]);
buf = this.writeBytesToScriptBuffer(buf, sigsList[y]);
break; //ensures duplicate sigs from same pubkey are not added
}
}
}
//append redeemscript
buf.push(redeemScript.length);
buf = buf.concat(redeemScript);
buf = this.writeBytesToScriptBuffer(buf, redeemScript);
this.inputs[index].script = buf;
return true;
@ -4992,6 +5014,78 @@
return Crypto.util.bytesToHex(buffer);
}
/* deserialize a transaction */
function deserialize(buffer) {
if (typeof buffer == "string") {
buffer = Crypto.util.hexToBytes(buffer)
}
var pos = 0;
var readAsInt = function (bytes) {
if (bytes == 0) return 0;
pos++;
return buffer[pos - 1] + readAsInt(bytes - 1) * 256;
}
var readVarInt = function () {
pos++;
if (buffer[pos - 1] < 253) {
return buffer[pos - 1];
}
return readAsInt(buffer[pos - 1] - 251);
}
var readBytes = function (bytes) {
pos += bytes;
return buffer.slice(pos - bytes, pos);
}
var readVarString = function () {
var size = readVarInt();
return readBytes(size);
}
var bytesToStr = function (bytes) {
return bytes.map(b => String.fromCharCode(b)).join('');
}
const self = btrx;
self.version = readAsInt(4);
var ins = readVarInt();
for (var i = 0; i < ins; i++) {
self.inputs.push({
outpoint: {
hash: Crypto.util.bytesToHex(readBytes(32).reverse()),
index: readAsInt(4)
},
script: readVarString(),
sequence: readAsInt(4)
});
}
var outs = readVarInt();
for (var i = 0; i < outs; i++) {
self.outputs.push({
value: bitjs.bytesToNum(readBytes(8)),
script: readVarString()
});
}
self.lock_time = readAsInt(4);
//flochange - floData field
self.floData = bytesToStr(readVarString());
return self;
}
//deserialize the data if passed
if (tx_data)
deserialize(tx_data);
return btrx;
}
@ -5229,23 +5323,22 @@
if ("string" == typeof bytes) {
var d = Bitcoin.Address.decodeString(bytes);
bytes = d.hash;
if (GLOBAL.cryptocoin == "FLO" && (d.version == Bitcoin.Address.standardVersion || d.version == Bitcoin.Address.multisigVersion))
this.version = d.version;
else if (GLOBAL.cryptocoin == "FLO_TEST" && d.version == Bitcoin.Address.testnetVersion)
if (d.version == Bitcoin.Address.standardVersion || d.version == Bitcoin.Address.multisigVersion)
this.version = d.version;
else throw "Version (prefix) " + d.version + " not supported!";
} else {
if (GLOBAL.cryptocoin == "FLO")
this.version = Bitcoin.Address.standardVersion;
else if (GLOBAL.cryptocoin == "FLO_TEST")
this.version = Bitcoin.Address.testnetVersion; // FLO testnet public address
this.version = Bitcoin.Address.standardVersion;
}
this.hash = bytes;
};
Bitcoin.Address.standardVersion = 0x23; // (FLO mainnet 0x23, 35D), (Bitcoin Mainnet, 0x00, 0D)
Bitcoin.Address.multisigVersion = 0x5e; // (FLO multisig 0x5e, 94D)
Bitcoin.Address.testnetVersion = 0x73; // (FLO testnet 0x73, 115D)
if (GLOBAL.cryptocoin == "FLO_TEST") {
Bitcoin.Address.standardVersion = 0x73; // (FLO testnet 0x73, 115D), (Bitcoin Mainnet, 0x00, 0D)
Bitcoin.Address.multisigVersion = 0xc6; // (FLO testnet multisig 0xc6, 198D)
}
/**
* Serialize this object as a standard Bitcoin address.
@ -6704,6 +6797,7 @@
return {
'address': address,
'redeemScript': r.redeemScript,
'scripthash': Crypto.util.bytesToHex(program),
'size': r.size
};
}
@ -6797,15 +6891,16 @@
};
}
coinjs.multisigBech32Address = function (raw_redeemscript) {
var program = Crypto.SHA256(Crypto.util.hexToBytes(raw_redeemscript), {
coinjs.multisigBech32Address = function (redeemscript) {
var program = Crypto.SHA256(Crypto.util.hexToBytes(redeemscript), {
asBytes: true
});
var address = coinjs.bech32_encode(coinjs.bech32.hrp, [coinjs.bech32.version].concat(coinjs.bech32_convert(program, 8, 5, true)));
return {
'address': address,
'type': 'multisigBech32',
'redeemscript': Crypto.util.bytesToHex(program)
'redeemScript': redeemscript,
'scripthash': Crypto.util.bytesToHex(program)
};
}
@ -7803,7 +7898,7 @@
var n = u.getElementsByTagName("tx_output_n")[0].childNodes[0].nodeValue;
var scr = script || u.getElementsByTagName("script")[0].childNodes[0].nodeValue;
if (segwit) { //also for MULTISIG_BECH32 (p2wsh-multisig)(script = raw_redeemscript; for p2wsh-multisig)
if (segwit) { //also for MULTISIG_BECH32 (p2wsh-multisig)(script = redeemscript; for p2wsh-multisig)
/* this is a small hack to include the value with the redeemscript to make the signing procedure smoother.
It is not standard and removed during the signing procedure. */