Changeset View
Changeset View
Standalone View
Standalone View
apps/alias-server/src/alias.js
Show All 29 Lines | getAliasTxs: function (aliasTxHistory, aliasConstants) { | ||||
registrationOutputScript, | registrationOutputScript, | ||||
); | ); | ||||
if (parsedAliasTx) { | if (parsedAliasTx) { | ||||
aliasTxs.push(parsedAliasTx); | aliasTxs.push(parsedAliasTx); | ||||
} | } | ||||
} | } | ||||
return aliasTxs; | return aliasTxs; | ||||
}, | }, | ||||
/** | |||||
* Parse a single transaction as returned by chronik tx history endpoint | |||||
* for valid alias registration(s) | |||||
* @param {object} aliasTx Object returned by chronik's tx history endpoint; must be a tx sent to the alias registration address per the spec | |||||
* @param {object} aliasConstants Object used to determine alias pricing and registration address | |||||
* @param {string} registrationOutputScript the hash160 of the alias registration address | |||||
* @returns {array} array of valid aliases registered in this tx if any | |||||
* Might always just be one. But need to handle edge case of multiple OP_RETURNs being mined. | |||||
* @returns {bool} false if the tx is not a valid alias registration | |||||
*/ | |||||
parseAliasTx: function (aliasTx, aliasConstants, registrationOutputScript) { | parseAliasTx: function (aliasTx, aliasConstants, registrationOutputScript) { | ||||
// Input: a single tx from chronik tx history | /** | ||||
// output: false if invalid tx | * Determine if there is a valid alias registration OP_RETURN output | ||||
// output: {address: 'address', alias: 'alias', txid} if valid | * - Is this an OP_RETURN output? | ||||
// validate for alias tx | * - Is version 0 alias registration bit pushed correctly? | ||||
* - Is the registered alias alphanumeric between 1 and 21 characters? | |||||
const inputZeroOutputScript = aliasTx.inputs[0].outputScript; | * - Is the registered address specified correctly (and valid) ? | ||||
* - Is the fee paid appropriate for the length? | |||||
const registeringAddress = cashaddr.encodeOutputScript( | * | ||||
inputZeroOutputScript, | * If the answer to all of these questions is "yes", then we have a valid alias | ||||
); | */ | ||||
// Initialize vars used later for validation | // Initialize variables that will be needed after iterating over outputs | ||||
let aliasFeePaidSats = BigInt(0); | let aliasFeePaidSats = BigInt(0); | ||||
let alias; | const validAliases = []; | ||||
let aliasLength; | |||||
// Iterate over outputs | // Iterate over outputs | ||||
const outputs = aliasTx.outputs; | const outputs = aliasTx.outputs; | ||||
for (let i = 0; i < outputs.length; i += 1) { | for (let i = 0; i < outputs.length; i += 1) { | ||||
const { value, outputScript } = outputs[i]; | const { value, outputScript } = outputs[i]; | ||||
// If value is 0, parse for OP_RETURN | if (outputScript.startsWith('6a')) { | ||||
if (value === '0') { | // If this is an OP_RETURN output, parse as a stack of data to determine | ||||
// whether or not this is a valid alias registration | |||||
// Remove '6a' | |||||
let stack = outputScript.slice(2); | |||||
// Check for valid alias prefix | // Check for valid alias prefix | ||||
// Note that the alias prefix must be pushed with 04 | |||||
const validAliasPrefix = | const validAliasPrefix = | ||||
outputScript.slice(0, 12) === | stack.slice(0, 10) === `04${aliasConstants.opCodePrefix}`; | ||||
`6a04${aliasConstants.opCodePrefix}`; | |||||
if (!validAliasPrefix) { | if (!validAliasPrefix) { | ||||
return false; | console.log(`Invalid prefix`, aliasTx.txid); | ||||
// If the prefix is invalid, this is not a valid alias registration | |||||
// Check the next output | |||||
continue; | |||||
} | } | ||||
// Check for valid alias length | // Remove `04${aliasConstants.opCodePrefix}` from the stack | ||||
const aliasLengthHex = outputScript.slice(12, 14); | stack = stack.slice(10); | ||||
aliasLength = parseInt(aliasLengthHex, 16); | // Check for valid version byte | ||||
const aliasRegistrationVersionByte = stack.slice(0, 2); | |||||
// Parse for the alias | if (aliasRegistrationVersionByte !== '00') { | ||||
const aliasHex = outputScript.slice(14, outputScript.length); | console.log( | ||||
alias = getAliasFromHex(aliasHex); | `Invalid alias registration version byte`, | ||||
aliasTx.txid, | |||||
); | |||||
// For now, only version 0 alias registration txs are valid | |||||
// If the version byte is not valid, this is not a valid alias registration | |||||
// Check the next output | |||||
continue; | |||||
} | |||||
// Remove '00' from the stack | |||||
stack = stack.slice(2); | |||||
// Check for length of alias | |||||
// Get the next byte from the stack | |||||
const aliasLengthBytesHex = stack.slice(0, 2); | |||||
const aliasLengthBytesDecimal = parseInt( | |||||
aliasLengthBytesHex, | |||||
16, | |||||
); | |||||
// Note: we only support pushing the alias with a length byte | |||||
// 4c + lengthbyte and alt push options are not supported | |||||
// Validate aliasLengthBytes | |||||
// Must be between 1 and 21 bytes inclusive | |||||
const isValidAliasLength = | |||||
typeof aliasLengthBytesDecimal === 'number' && | |||||
aliasLengthBytesDecimal <= 21 && | |||||
aliasLengthBytesDecimal >= 1; | |||||
if (!isValidAliasLength) { | |||||
console.log( | |||||
`Invalid alias length ${aliasLengthBytesDecimal}`, | |||||
aliasTx.txid, | |||||
); | |||||
// If the registered alias is not of a valid length, | |||||
// this is not a valid alias registration | |||||
// Check the next output | |||||
continue; | |||||
} | |||||
// Remove aliasLengthBytesHex from the stack | |||||
stack = stack.slice(2); | |||||
// Get aliasHex from the stack | |||||
const aliasHex = stack.slice(0, 2 * aliasLengthBytesDecimal); | |||||
// Convert to utf8 string | |||||
const thisAlias = getAliasFromHex(aliasHex); | |||||
// Check for valid character set | // Check for valid character set | ||||
// only lower case roman alphabet a-z | // only lower case roman alphabet a-z | ||||
// numbers 0 through 9 | // numbers 0 through 9 | ||||
if (!isValidAliasString(alias)) { | if (!isValidAliasString(thisAlias)) { | ||||
return false; | console.log(`Invalid alias string`, aliasTx.txid); | ||||
continue; | |||||
} | } | ||||
const validAliasLength = | // Remove aliasHex from the stack | ||||
aliasLength <= aliasConstants.maxLength && | stack = stack.slice(2 * aliasLengthBytesDecimal); | ||||
aliasHex.length === 2 * aliasLength; | |||||
// Get the length of address version byte + address | |||||
// This will almost always be '0x15', i.e. 21 in decimal | |||||
// But it may be other lengths depending on the address being registered | |||||
const addressLengthInBytesHex = stack.slice(0, 2); | |||||
const addressLengthInBytesDecimal = parseInt( | |||||
addressLengthInBytesHex, | |||||
16, | |||||
); | |||||
// Remove addressLengthInBytesHex from the stack | |||||
stack = stack.slice(2); | |||||
if (!validAliasLength) { | // Get the address push | ||||
return false; | const addressPushHex = stack.slice( | ||||
0, | |||||
2 * addressLengthInBytesDecimal, | |||||
); | |||||
// Note that the first byte of the address push is the address type | |||||
const addressTypeByte = addressPushHex.slice(0, 2); | |||||
// Determine the address type | |||||
let addressType; | |||||
switch (addressTypeByte) { | |||||
case '00': { | |||||
addressType = 'p2pkh'; | |||||
break; | |||||
} | |||||
case '08': { | |||||
addressType = 'p2sh'; | |||||
break; | |||||
} | |||||
default: { | |||||
// Invalid alias registration, stop processing this OP_RETURN output | |||||
continue; | |||||
} | |||||
} | } | ||||
// Get the address hash | |||||
// This is the rest of addressPushHex after the address type byte | |||||
const addressHash = addressPushHex.slice(2); | |||||
// Now that you have type and hash, get the ecash address | |||||
let thisAddress; | |||||
try { | |||||
thisAddress = cashaddr.encode( | |||||
'ecash', | |||||
addressType, | |||||
addressHash, | |||||
); | |||||
} catch (err) { | |||||
console.log(`Invalid cash address`, aliasTx.txid); | |||||
// If the type and hash do not constitute a valid cashaddr, this OP_RETURN is not a valid alias | |||||
// Check the next output | |||||
continue; | |||||
} | |||||
// If you get here, everything about the construction of the registration in the OP_RETURN field is valid | |||||
// However you still must compare against fee paid and registration history to finalize | |||||
validAliases.push({ alias: thisAlias, address: thisAddress }); | |||||
// Note, you may still have more data on the stack. Intentionally ignored, does not invalidate the registration. | |||||
} else { | } else { | ||||
// Check if outputScript matches alias registration address | // Check if outputScript matches alias registration address | ||||
if (outputScript === registrationOutputScript) | if (outputScript === registrationOutputScript) | ||||
// If so, then the value here is part of the alias registration fee, aliasFeePaidSats | // If so, then the value here is part of the alias registration fee, aliasFeePaidSats | ||||
aliasFeePaidSats += BigInt(value); | aliasFeePaidSats += BigInt(value); | ||||
} | } | ||||
} | } | ||||
// If `alias` is undefined after the above loop, then this is not a valid alias registration tx | |||||
if (typeof alias === 'undefined') { | if (validAliases.length !== 1) { | ||||
/** | |||||
* A valid alias registration tx must have only one valid OP_RETURN | |||||
* TODO update spec | |||||
*/ | |||||
return false; | return false; | ||||
} | } | ||||
// Get the alias and its corresponding address | |||||
const { address, alias } = validAliases[0]; | |||||
if (aliasFeePaidSats > aliasConstants.registrationFeesSats) { | |||||
console.log(`Overpaid fee`, aliasTx.txid); | |||||
} | |||||
// Confirm that the correct fee is paid to the correct address | // Confirm that the correct fee is paid to the correct address | ||||
if ( | if ( | ||||
parseInt(aliasFeePaidSats) < | // Note, fee is technically for bytes | ||||
aliasConstants.registrationFeesSats[aliasLength] | // For alphanumeric aliases, it's okay to use alias.length to represent bytes | ||||
aliasFeePaidSats < aliasConstants.registrationFeesSats[alias.length] | |||||
) { | ) { | ||||
console.log( | // If the fee was underpaid, this is not a valid alias registration | ||||
`Invalid fee. This transaction paid ${aliasFeePaidSats} sats to register ${alias}. The correct fee for an alias of ${aliasLength} characters is ${aliasConstants.registrationFeesSats[aliasLength]}`, | console.log(`Underpaid fee`, aliasTx.txid); | ||||
); | |||||
return false; | return false; | ||||
} | } | ||||
return { | return { | ||||
address: registeringAddress, | address, | ||||
alias, | alias, | ||||
txid: aliasTx.txid, | txid: aliasTx.txid, | ||||
// arbitrary to set unconfirmed txs at blockheight of 100,000,000 | // arbitrary to set unconfirmed txs at blockheight of 100,000,000 | ||||
// note that this constant must be adjusted in the fall of 3910 A.D., assuming 10 min blocks | // note that this constant must be adjusted in the fall of 3910 A.D., assuming 10 min blocks | ||||
// setting it high instead of zero because it's important we sort aliases by blockheight | // setting it high instead of zero because it's important we sort aliases by blockheight | ||||
// for sortAliasTxsByTxidAndBlockheight function | // for sortAliasTxsByTxidAndBlockheight function | ||||
blockheight: | blockheight: | ||||
aliasTx && aliasTx.block | aliasTx && aliasTx.block | ||||
▲ Show 20 Lines • Show All 71 Lines • Show Last 20 Lines |