lotus-sdk
Version:
Central repository for several classes of tools for integrating with, and building for, the Lotusia ecosystem
336 lines (335 loc) • 14.4 kB
JavaScript
import { Signature } from '../crypto/signature.js';
import { Script, empty } from '../script.js';
import { Output } from './output.js';
import { BufferReader } from '../encoding/bufferreader.js';
import { BufferWriter } from '../encoding/bufferwriter.js';
import { BN } from '../crypto/bn.js';
import { Hash } from '../crypto/hash.js';
import { ECDSA } from '../crypto/ecdsa.js';
import { Schnorr } from '../crypto/schnorr.js';
import { Preconditions } from '../util/preconditions.js';
import { BufferUtil } from '../util/buffer.js';
import { Interpreter } from '../script/interpreter.js';
import { Transaction } from './transaction.js';
import { Input } from './input.js';
const SIGHASH_SINGLE_BUG_CONST = '0000000000000000000000000000000000000000000000000000000000000001';
const BITS_64_ON_CONST = 'ffffffffffffffff';
const NULL_HASH = Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex');
const SIGHASH_ALGORITHM_MASK = 0x60;
const DEFAULT_SIGN_FLAGS_CONST = 1 << 16;
function GetForkId() {
return 0;
}
function getMerkleRoot(hashes) {
if (hashes.length === 0) {
return { root: NULL_HASH, height: 0 };
}
let currentHashes = [...hashes];
let height = 1;
while (currentHashes.length > 1) {
height++;
const newHashes = [];
for (let i = 0; i < currentHashes.length; i += 2) {
const left = currentHashes[i];
const right = i + 1 < currentHashes.length ? currentHashes[i + 1] : NULL_HASH;
const combined = Buffer.concat([left, right]);
const pairHash = Hash.sha256sha256(combined);
newHashes.push(pairHash);
}
currentHashes = newHashes;
}
return { root: currentHashes[0], height };
}
function sighashForLotus(transaction, sighashType, inputNumber, spentOutputs, executedScriptHash, codeseparatorPos = 0xffffffff) {
Preconditions.checkArgument(spentOutputs.length === transaction.inputs.length, 'Must provide spent output for each input');
Preconditions.checkArgument(inputNumber < transaction.inputs.length, 'Input index out of range');
const baseType = sighashType & 0x03;
const unusedBits = sighashType & 0x1c;
if (baseType === 0 || unusedBits !== 0) {
throw new Error('Invalid sighash type for SIGHASH_LOTUS');
}
const input = transaction.inputs[inputNumber];
const writer = new BufferWriter();
writer.writeUInt32LE(sighashType >>> 0);
const spendType = executedScriptHash ? 2 : 0;
const inputHashWriter = new BufferWriter();
inputHashWriter.writeUInt8(spendType);
inputHashWriter.writeReverse(input.prevTxId);
inputHashWriter.writeUInt32LE(input.outputIndex);
inputHashWriter.writeUInt32LE(input.sequenceNumber);
const spentOutput = spentOutputs[inputNumber];
inputHashWriter.writeUInt64LEBN(new BN(spentOutput.satoshis));
inputHashWriter.writeVarLengthBuffer(spentOutput.scriptBuffer);
const inputHash = Hash.sha256sha256(inputHashWriter.toBuffer());
writer.write(inputHash);
if (executedScriptHash) {
Preconditions.checkArgument(executedScriptHash.length === 32, 'executed_script_hash must be 32 bytes');
writer.writeUInt32LE(codeseparatorPos);
writer.write(executedScriptHash);
}
if (!(sighashType & Signature.SIGHASH_ANYONECANPAY)) {
writer.writeUInt32LE(inputNumber);
const spentOutputHashes = spentOutputs.map(output => {
const w = new BufferWriter();
w.writeUInt64LEBN(new BN(output.satoshis));
w.writeVarLengthBuffer(output.scriptBuffer);
return Hash.sha256sha256(w.toBuffer());
});
const spentOutputsMerkle = getMerkleRoot(spentOutputHashes);
writer.write(spentOutputsMerkle.root);
const totalInputAmount = spentOutputs.reduce((sum, output) => sum + output.satoshis, 0);
writer.writeUInt64LEBN(new BN(totalInputAmount));
}
if (baseType === Signature.SIGHASH_ALL) {
const totalOutputAmount = transaction.outputs.reduce((sum, output) => sum + output.satoshis, 0);
writer.writeUInt64LEBN(new BN(totalOutputAmount));
}
writer.writeUInt32LE(transaction.version || 2);
if (!(sighashType & Signature.SIGHASH_ANYONECANPAY)) {
const inputHashes = transaction.inputs.map(inp => {
const w = new BufferWriter();
w.writeReverse(inp.prevTxId);
w.writeUInt32LE(inp.outputIndex);
w.writeUInt32LE(inp.sequenceNumber);
return Hash.sha256sha256(w.toBuffer());
});
const inputsMerkle = getMerkleRoot(inputHashes);
writer.write(inputsMerkle.root);
writer.writeUInt8(inputsMerkle.height);
}
if (baseType === Signature.SIGHASH_SINGLE) {
if (inputNumber >= transaction.outputs.length) {
throw new Error('SIGHASH_SINGLE: no corresponding output for input');
}
const w = new BufferWriter();
transaction.outputs[inputNumber].toBufferWriter(w);
const outputHash = Hash.sha256sha256(w.toBuffer());
writer.write(outputHash);
}
if (baseType === Signature.SIGHASH_ALL) {
const outputHashes = transaction.outputs.map(output => {
const w = new BufferWriter();
output.toBufferWriter(w);
return Hash.sha256sha256(w.toBuffer());
});
const outputsMerkle = getMerkleRoot(outputHashes);
writer.write(outputsMerkle.root);
writer.writeUInt8(outputsMerkle.height);
}
writer.writeUInt32LE(transaction.nLockTime || 0);
return Hash.sha256sha256(writer.toBuffer());
}
function sighashForForkId(transaction, sighashType, inputNumber, subscript, satoshisBN) {
const input = transaction.inputs[inputNumber];
Preconditions.checkArgument(satoshisBN instanceof BN, 'For ForkId=0 signatures, satoshis or complete input must be provided');
let hashPrevouts = BufferUtil.emptyBuffer(32);
let hashSequence = BufferUtil.emptyBuffer(32);
let hashOutputs = BufferUtil.emptyBuffer(32);
if (!(sighashType & Signature.SIGHASH_ANYONECANPAY)) {
hashPrevouts = GetPrevoutHash(transaction);
}
if (!(sighashType & Signature.SIGHASH_ANYONECANPAY) &&
(sighashType & 31) !== Signature.SIGHASH_SINGLE &&
(sighashType & 31) !== Signature.SIGHASH_NONE) {
hashSequence = GetSequenceHash(transaction);
}
if ((sighashType & 31) !== Signature.SIGHASH_SINGLE &&
(sighashType & 31) !== Signature.SIGHASH_NONE) {
hashOutputs = GetOutputsHash(transaction);
}
else if ((sighashType & 31) === Signature.SIGHASH_SINGLE &&
inputNumber < transaction.outputs.length) {
hashOutputs = GetOutputsHash(transaction, inputNumber);
}
const writer = new BufferWriter();
writer.writeUInt32LE(transaction.version || 2);
writer.write(hashPrevouts);
writer.write(hashSequence);
writer.writeReverse(input.prevTxId);
writer.writeUInt32LE(input.outputIndex);
writer.writeVarintNum(subscript.toBuffer().length);
writer.write(subscript.toBuffer());
writer.writeUInt64LEBN(satoshisBN);
writer.writeUInt32LENumber(input.sequenceNumber);
writer.write(hashOutputs);
writer.writeUInt32LE(transaction.nLockTime || 0);
writer.writeUInt32LE(sighashType >>> 0);
const buf = writer.toBuffer();
const hash = Hash.sha256sha256(buf);
return new BufferReader(hash).read(32);
}
function sighashLegacy(transaction, sighashType, inputNumber, subscript) {
const input = transaction.inputs[inputNumber];
function getHash(w) {
const buf = w.toBuffer();
return Hash.sha256sha256(buf);
}
const writer = new BufferWriter();
writer.writeUInt32LE(2);
writer.writeVarintNum(transaction.inputs.length);
for (let i = 0; i < transaction.inputs.length; i++) {
const txInput = transaction.inputs[i];
writer.writeReverse(txInput.prevTxId);
writer.writeUInt32LE(txInput.outputIndex);
if (i === inputNumber) {
writer.writeVarLengthBuffer(subscript.toBuffer());
}
else {
writer.writeVarintNum(0);
}
writer.writeUInt32LENumber(txInput.sequenceNumber);
}
writer.writeVarintNum(transaction.outputs.length);
if ((sighashType & 31) !== Signature.SIGHASH_SINGLE &&
(sighashType & 31) !== Signature.SIGHASH_NONE) {
for (const output of transaction.outputs) {
output.toBufferWriter(writer);
}
}
else if ((sighashType & 31) === Signature.SIGHASH_SINGLE &&
inputNumber < transaction.outputs.length) {
transaction.outputs[inputNumber].toBufferWriter(writer);
}
writer.writeUInt32LE(transaction.nLockTime || 0);
writer.writeUInt32LE(sighashType);
return getHash(writer);
}
function sighash(transaction, sighashType, inputNumber, subscript, satoshisBN, flags) {
if (flags === undefined) {
flags = DEFAULT_SIGN_FLAGS_CONST;
}
const txcopy = Transaction.shallowCopy(transaction);
subscript = new Script(subscript);
if (flags & Interpreter.SCRIPT_ENABLE_REPLAY_PROTECTION) {
const forkValue = sighashType >> 8;
const newForkValue = 0xff0000 | (forkValue ^ 0xdead);
sighashType = (newForkValue << 8) | (sighashType & 0xff);
}
const algorithmBits = sighashType & SIGHASH_ALGORITHM_MASK;
if (algorithmBits === Signature.SIGHASH_LOTUS &&
flags & Interpreter.SCRIPT_ENABLE_SIGHASH_FORKID) {
const spentOutputs = transaction.spentOutputs;
if (!spentOutputs || spentOutputs.length !== transaction.inputs.length) {
throw new Error('SIGHASH_LOTUS requires spent outputs for all inputs (ensure all inputs have output information)');
}
return sighashForLotus(txcopy, sighashType, inputNumber, spentOutputs);
}
if (algorithmBits === Signature.SIGHASH_FORKID &&
flags & Interpreter.SCRIPT_ENABLE_SIGHASH_FORKID) {
return sighashForForkId(txcopy, sighashType, inputNumber, subscript, satoshisBN);
}
subscript.removeCodeseparators();
for (let i = 0; i < txcopy.inputs.length; i++) {
txcopy.inputs[i] = new Input({
prevTxId: txcopy.inputs[i].prevTxId,
outputIndex: txcopy.inputs[i].outputIndex,
sequenceNumber: txcopy.inputs[i].sequenceNumber,
script: empty(),
});
}
txcopy.inputs[inputNumber] = new Input({
prevTxId: txcopy.inputs[inputNumber].prevTxId,
outputIndex: txcopy.inputs[inputNumber].outputIndex,
sequenceNumber: txcopy.inputs[inputNumber].sequenceNumber,
script: subscript,
});
if ((sighashType & 31) === Signature.SIGHASH_NONE ||
(sighashType & 31) === Signature.SIGHASH_SINGLE) {
for (let i = 0; i < txcopy.inputs.length; i++) {
if (i !== inputNumber) {
txcopy.inputs[i].sequenceNumber = 0;
}
}
}
if ((sighashType & 31) === Signature.SIGHASH_NONE) {
txcopy.outputs = [];
}
else if ((sighashType & 31) === Signature.SIGHASH_SINGLE) {
if (inputNumber >= txcopy.outputs.length) {
return Buffer.from(SIGHASH_SINGLE_BUG_CONST, 'hex');
}
txcopy.outputs.length = inputNumber + 1;
for (let i = 0; i < inputNumber; i++) {
txcopy.outputs[i] = new Output({
satoshis: BN.fromBuffer(Buffer.from(BITS_64_ON_CONST, 'hex')),
script: empty(),
});
}
}
if (sighashType & Signature.SIGHASH_ANYONECANPAY) {
txcopy.inputs = [txcopy.inputs[inputNumber]];
}
const buf = new BufferWriter()
.write(txcopy.toBuffer())
.writeInt32LE(sighashType >>> 0)
.toBuffer();
const hash = Hash.sha256sha256(buf);
return new BufferReader(hash).readReverse(32);
}
function sign(transaction, privateKey, sighashType, inputIndex, subscript, satoshisBN, flags, signingMethod) {
const hashbuf = sighash(transaction, sighashType, inputIndex, subscript, satoshisBN, flags);
signingMethod = signingMethod || 'ecdsa';
let sig;
if (signingMethod === 'schnorr') {
sig = Schnorr.sign(hashbuf, privateKey, 'big');
sig.nhashtype = sighashType;
return sig;
}
else if (signingMethod === 'ecdsa') {
sig = ECDSA.sign(hashbuf, privateKey, 'big');
sig.nhashtype = sighashType;
return sig;
}
else {
throw new Error('Invalid signing method. Must be "ecdsa" or "schnorr"');
}
}
function verify(transaction, signature, publicKey, inputIndex, subscript, satoshisBN, flags, signingMethod) {
Preconditions.checkArgument(transaction !== undefined, 'Transaction is required');
Preconditions.checkArgument(signature !== undefined && signature.nhashtype !== undefined, 'Signature with nhashtype is required');
const hashbuf = sighash(transaction, signature.nhashtype, inputIndex, subscript, satoshisBN, flags);
signingMethod = signingMethod || 'ecdsa';
if (signingMethod === 'schnorr') {
return Schnorr.verify(hashbuf, signature, publicKey, 'big');
}
else if (signingMethod === 'ecdsa') {
return ECDSA.verify(hashbuf, signature, publicKey, 'big');
}
else {
throw new Error('Invalid signing method. Must be "ecdsa" or "schnorr"');
}
}
function GetPrevoutHash(tx) {
const writer = new BufferWriter();
for (const input of tx.inputs) {
writer.writeReverse(input.prevTxId);
writer.writeUInt32LE(input.outputIndex);
}
const buf = writer.toBuffer();
return Hash.sha256sha256(buf);
}
function GetSequenceHash(tx) {
const writer = new BufferWriter();
for (const input of tx.inputs) {
writer.writeUInt32LENumber(input.sequenceNumber);
}
const buf = writer.toBuffer();
return Hash.sha256sha256(buf);
}
function GetOutputsHash(tx, n) {
const writer = new BufferWriter();
if (n === undefined) {
for (const output of tx.outputs) {
output.toBufferWriter(writer);
}
}
else {
tx.outputs[n].toBufferWriter(writer);
}
const buf = writer.toBuffer();
return Hash.sha256sha256(buf);
}
export const DEFAULT_SIGN_FLAGS = DEFAULT_SIGN_FLAGS_CONST;
export const SIGHASH_SINGLE_BUG = SIGHASH_SINGLE_BUG_CONST;
export const BITS_64_ON = BITS_64_ON_CONST;
export { sighash, sign, verify };