Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decode sstxcommitment script #2523

Merged
merged 4 commits into from
Jul 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 4 additions & 18 deletions app/constants/Decred.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,21 +116,7 @@ export const STSchnorrSecp256k1 = 2;
// ripemd160Size is the size of the RIPEMD-160 hash algorithm checksum in bytes.
export const ripemd160Size = 20;

// OP_CODES
export const OP_0 = 0x00; // 0
export const OP_1 = 0x51; // 81 - AKA OP_TRUE
export const OP_16 = 0x60; // 96
export const OP_DUP = 0x76; // 118
export const OP_HASH160 = 0xa9; // 169
export const OP_DATA_20 = 0x14; // 20
export const OP_EQUAL = 0x87; // 135
export const OP_EQUALVERIFY = 0x88; // 136
export const OP_CHECKSIG = 0xac; // 172
export const OP_SSTX = 0xba; // 186 DECRED
export const OP_SSGEN = 0xbb; // 187 DECRED
export const OP_SSRTX = 0xbc; // 188 DECRED
export const OP_SSTXCHANGE = 0xbd; // 189 DECRED
export const OP_DATA_33 = 0x21; // 33
export const OP_DATA_65 = 0x41; // 65
export const OP_CHECKSIGALT = 0xbe; // 190 DECRED
export const OP_DATA_45 = 0x2d; // 45
// SStxPKHMinOutSize is the minimum size of an OP_RETURN commitment output
// for an SStx tx.
// 20 bytes P2SH/P2PKH + 8 byte amount + 4 byte fee range limits
export const SStxPKHMinOutSize = 32;
1 change: 1 addition & 0 deletions app/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "./Decred";
export * from "./Decrediton";
export * from "./Messages";
export * from "./config";
export * from "./opcode";
526 changes: 526 additions & 0 deletions app/constants/opcode.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions app/helpers/addresses.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ const checksum = (input) => {
};

// checkEncode prepends two version bytes and appends a four byte checksum.
const checkEncode = (input, version) => {
export const checkEncode = (input, version) => {
let b = Buffer.from(version);
b = Buffer.concat([b, input]);
const calculatedChecksum = checksum(b);
Expand Down Expand Up @@ -149,7 +149,7 @@ export const newAddressScriptHashFromHash = (scriptHash, params) => {
// known.
const getNewAddressScriptHashFromHash = (scriptHash, netID) => {
// Check for a valid script hash length.
if (scriptHash.length != ripemd160Size) {
if (scriptHash.length !== ripemd160Size) {
return { error: "pkHash must be 20 bytes" };
}

Expand Down
12 changes: 7 additions & 5 deletions app/helpers/msgTx.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
putUint8,
putUint16,
putUint32,
putUint64
putUint64,
hexToBytes
} from "./byteActions";
import { Uint64LE } from "int64-buffer";

Expand Down Expand Up @@ -136,8 +137,9 @@ function serializeSize(output) {
// writeOutPoint encodes op to the Decred protocol encoding for an OutPoint
// to w.
function writeOutPoint(input, arr8, position) {
arr8.set(input.opRawHash, position);
position += input.opRawHash.length;
const opRawHash = hexToBytes(input.prevTxId);
arr8.set(opRawHash, position);
position += opRawHash.length;
arr8.set(putUint32(input.outputIndex), position);
position += 4;
arr8.set(putUint8(input.outputTree), position);
Expand Down Expand Up @@ -197,8 +199,8 @@ export function decodeRawTransaction(rawTx) {
tx.inputs = [];
for (let i = 0; i < tx.numInputs; i++) {
const input = {};
input.opRawHash = rawTx.slice(position, position + 32);
input.prevTxId = reverseHash(rawToHex(input.opRawHash));
const opRawHash = rawTx.slice(position, position + 32);
input.prevTxId = reverseHash(rawToHex(opRawHash));
position += 32;
input.outputIndex = rawTx.readUInt32LE(position);
position += 4;
Expand Down
144 changes: 138 additions & 6 deletions app/helpers/scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import {
OP_SSTXCHANGE,
OP_DATA_33,
OP_DATA_65,
OP_CHECKSIGALT
OP_CHECKSIGALT,
OP_PUSHDATA4,
opcodeArray,
OP_RETURN
} from "constants";

const MaxUint16 = 1 << (16 - 1);
Expand Down Expand Up @@ -429,11 +432,23 @@ export const extractPkScriptAddrs = (version, pkScript, chainParams) => {
};
}

// // Check for null data script.
// if isNullDataScript(version, pkScript) {
// // Null data transactions have no addresses or required signatures.
// return NullDataTy, nil, 0, nil
// }
const parsedScript = parseScript(pkScript, opcodeArray);
let pops;
if (parsedScript) {
const { error, retScript } = parsedScript;
if (error) return error;
pops = retScript;
}
// Check for null data script.
if (isNullData(pops)) {
// Null data transactions have no addresses or required signatures.
return {
// scriptclass NullDataTy
scriptClass: 0,
address: [],
requiredSig: 0
};
}

// Don't attempt to extract addresses or required signatures for nonstandard
// transactions.
Expand Down Expand Up @@ -529,3 +544,120 @@ export const EstimateSerializeSizeFromScriptSizes = (
changeSize
);
};

// isNullData returns true if the passed script is a null data transaction,
// false otherwise.
const isNullData = (pops) => {
if (!pops) return;
// A nulldata transaction is either a single OP_RETURN or an
// OP_RETURN SMALLDATA (where SMALLDATA is a data push up to
// MaxDataCarrierSize bytes).
const MaxDataCarrierSize = 256;
const l = pops.length;
if (l === 1 && pops[0].opcode.value === OP_RETURN) {
return true;
}

return (
l === 2 &&
pops[0].opcode.value == OP_RETURN &&
(isSmallInt(pops[1].opcode.value) ||
pops[1].opcode.value <= OP_PUSHDATA4) &&
pops[1].data.length <= MaxDataCarrierSize
);
};

// parseScript parses a script getting all of its opcodes and which data they
// may have. This code was removed from dcrd due to Zero alloc optimization
// refactor optmization at txscript, but it is fine for decrediton as for now we
// dont decode big scripts on it.
// source: https://github.com/decred/dcrd/pull/1656/commits/fcb1f3a7a137f3d69091c23c0f349d35df6c1ee6
const parseScript = (script, opcodes) => {
if (!script) return;
const retScript = [];
for (let i = 0; i < script.length; i++) {
const instr = script[i];
const op = opcodes[instr];
const pop = { opcode: op };

if (op.length == 1) {
retScript.push(pop);
continue;
} else if (op.length > 1) {
if (script.slice(i).length < op.length) {
return {
retScript,
error: `opcode ${op.name} requires ${
op.length
} bytes, but script only has ${script.slice(i).length} remaining.`
};
}
pop.data = script.slice(i + 1, i + op.length);
i += op.length - 1;
} else if (op.legnth < 0) {
let l;
let off = i + 1;

// negativeLengthHelper is an aux method to help get data and move the offset
// of a script with negative length (little endian length) so we can get the
// data. This way we can avoid code repetition.
const negativeLengthHelper = () => {
// Move offset to beginning of the data.
off += -op.length;

// Disallow entries that do not fit script or were
// sign extended.
if (l > script.slice(off).length || l < 0) {
return {
retScript,
error: `opcode ${op.name} pushes ${l} bytes, but script only has ${
script.slice(off).length
} remaining`
};
}

pop.data = script.slice(off, off + l);
i += 1 - op.length + l;
};
if (script.slice(off).length < -op.length) {
return {
retScript,
error: `opcode ${
op.name
} requires ${-op.length} bytes, but script only has ${
script.slice(off).length
} remaining.`
};
}

// Next -length bytes are little endian length of data.
switch (op.length) {
case -1:
l = script[off];
negativeLengthHelper();
break;
case -2:
l = (script[off + 1] << 8) | script[off];
negativeLengthHelper();
break;
case -4:
l =
(script[off + 3] << 24) |
(script[off + 2] << 16) |
(script[off + 1] << 8) |
script[off];
negativeLengthHelper();
break;
default:
return {
retScript,
error: `invalid opcode length ${op.length}`
};
}
}

retScript.push(pop);
}

return { retScript };
};
76 changes: 75 additions & 1 deletion app/helpers/tickets.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { GetTicketsResponse } from "../middleware/walletrpc/api_pb";
import { OP_RETURN, SStxPKHMinOutSize, ripemd160Size } from "constants";
import { checkEncode } from "./addresses";

export const TicketTypes = new Map([
[GetTicketsResponse.TicketDetails.TicketStatus.UNKNOWN, "unknown"],
Expand All @@ -20,7 +22,7 @@ export const TicketTypes = new Map([
// Reference for what a voting script looks like (as of this writing):
// https://github.com/decred/dcrd/blob/3f3174c987091b03bb34f1fdf4614d10ce6fbfc5/blockchain/stake/staketx.go#L458
export function decodeVoteScript(network, outputScript) {
if (outputScript.length < 4 || outputScript[0] !== 0x6a) {
if (outputScript.length < 4 || outputScript[0] !== OP_RETURN) {
// 0x6a == OP_RETURN
return null;
}
Expand Down Expand Up @@ -137,3 +139,75 @@ export function decodeVoteScript(network, outputScript) {

return voteChoices;
}

// addrFromSStxPkScrCommitment extracts a P2SH or P2PKH address from a ticket
// commitment pkScript.
export const addrFromSStxPkScrCommitment = (pkScript, params) => {
if (pkScript.length < SStxPKHMinOutSize) {
return { error: `pkScript must be ${SStxPKHMinOutSize} bytes` };
}

// The MSB of the encoded amount specifies if the output is P2SH. Since
// it is encoded with little endian, the MSB is in final byte in the encoded
// amount.
//
// This is a faster equivalent of:
//
// amtBytes := script[22:30]
// amtEncoded := binary.LittleEndian.Uint64(amtBytes)
// isP2SH := (amtEncoded & uint64(1<<63)) != 0

const isP2SH = pkScript[29] & (0x80 != 0);
// The 20 byte PKH or SH.
const hashBytes = pkScript.slice(2, 22);

// Return the correct address type.
if (isP2SH) {
return newAddressScriptHashFromHash(hashBytes, params);
}
return newAddressPubKeyHash(hashBytes, params, 0);
};

// newAddressScriptHashFromHash is the internal API to create a script hash
// address with a known leading identifier byte for a network, rather than
// looking it up through its parameters. This is useful when creating a new
// address structure from a string encoding where the identifier byte is already
// known.
const newAddressScriptHashFromHash = (scriptHash, netId) => {
if (scriptHash.length !== ripemd160Size) {
return { error: "pkHash must be 20 bytes" };
}

return checkEncode(scriptHash.slice(0, 20), netId);
};

// newAddressPubKeyHash returns a new AddressPubKeyHash. pkHash must
// be 20 bytes.
const newAddressPubKeyHash = (scriptHash, net, algo) => {
// Ensure the provided signature algo is supported.
let addrID;
switch (algo) {
// when extracting address from a SStxPkScrCommitment script, the algo used
// is dcrec.STEcdsaSecp256k1 equals 0.
case 0:
addrID = net.PubKeyHashAddrID;
break;
// TODO finish getting address from pubkeyHash to add support to decrediton.
// case dcrec.STEd25519:
// addrID = net.AddrIDPubKeyHashEd25519V0()
// case dcrec.STSchnorrSecp256k1:
// addrID = net.AddrIDPubKeyHashSchnorrV0()
default:
return null;
}

// Ensure the provided pubkey hash length is valid.
if (scriptHash.length !== ripemd160Size) {
return { error: "pkHash must be 20 bytes" };
}

const addr = { netID: addrID };
addr.hash = scriptHash.slice(0, ripemd160Size);

return checkEncode(scriptHash.slice(0, 20), addrID);
};
15 changes: 13 additions & 2 deletions app/wallet/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
decodeRawTransaction as decodeHelper
} from "helpers";
import { extractPkScriptAddrs } from "helpers/scripts";
import { addrFromSStxPkScrCommitment } from "helpers/tickets";

const promisify = (fn) => (...args) =>
new Promise((ok, fail) =>
Expand Down Expand Up @@ -297,10 +298,20 @@ export const decodeRawTransaction = (rawTx, chainParams) => {
}

const decodedTx = decodeHelper(rawTx);
decodedTx.outputs = decodedTx.outputs.map((o) => {
decodedTx.outputs = decodedTx.outputs.map((o, i) => {
let decodedScript = extractPkScriptAddrs(0, o.script, chainParams);
// if scriptClass equals NullDataTy (which is 0) && i&1 == 1
// extract address from SStxPkScrCommitment script.
if (decodedScript.scriptClass === 0 && i&1 === 1) {
decodedScript = {
address: addrFromSStxPkScrCommitment(o.script, chainParams),
scriptClass: 0,
requiredSig: 0
};
}
return {
...o,
decodedScript: extractPkScriptAddrs(0, o.script, chainParams)
decodedScript
};
});

Expand Down
5 changes: 4 additions & 1 deletion test/data/HexTransactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -520,4 +520,7 @@ export const hugeMixedSplitTx = "010000006af37808f8f33aafcfd91f9158f110ea9dac7ca
"07844c37f8df455cf583a9789196828fdabcc0f7cc5f9ccda32911035ce8a5aa02203f00e66a8f6609" +
"cf63390d82f25f4cb4d35668172e43bd38d5bd91bffb90e1d40121036e19fde113020f8afe936d29db" +
"215053cf8513ce5bdde7a7c59d45541d69b498";




export const purchasedTicketTx = "0100000001846bc0a283d908e48c24899380dc1550e99548e6049dd79453c20a94f5f9a7690000000000ffffffff030b848d420100000000001aba76a9146ba1f7a65b7f3a1db3455e17b11b48fc55df25b788ac00000000000000000000206a1e267b1f1a005cec33d03338e6200ec6e616ed9d88af8f8d42010000000058000000000000000000001abd76a914000000000000000000000000000000000000000088ac0000000084c3060001af8f8d420100000000000000ffffffff6a473044022043f26897017cdb9cf61d9f13aa0ece87c1eb61c2cf83371568d4d2093830beff02203948589692b7a0dd3e504286afe9e309989f9f298c7e9070610c0b6f2aa873410121030875038ca21fee734f101888fef4315270e59d36eaff9830616e1e4dcf22f82b"
Loading