Changeset 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) { | ||||
Fabien: This variable is useless, just put the condition in the if() | |||||
return false; | // If the prefix is invalid, this is not a valid alias registration | ||||
// Check the next output | |||||
continue; | |||||
} | |||||
// Remove `04${aliasConstants.opCodePrefix}` from the stack | |||||
stack = stack.slice(10); | |||||
// Check for valid version byte | |||||
const aliasRegistrationVersionByte = stack.slice(0, 2); | |||||
if (aliasRegistrationVersionByte !== '00') { | |||||
// 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); | |||||
FabienUnsubmitted Done Inline ActionsYou should consider a consume(nbytes) method that returns the n bytes and move the cursor. Like: if (consume(stack, 2) != '00') { // For now, only version 0 alias registration txs are valid continue; } Fabien: You should consider a `consume(nbytes)` method that returns the n bytes and move the cursor. | |||||
// Check for length of alias | |||||
// Get the next byte from the stack | |||||
const aliasLengthBytesHex = stack.slice(0, 2); | |||||
const aliasLengthBytesDecimal = parseInt( | |||||
aliasLengthBytesHex, | |||||
FabienUnsubmitted Done Inline ActionsThis intermediate variable is not useful Fabien: This intermediate variable is not useful | |||||
16, | |||||
); | |||||
// Note: we only support pushing the alias with a length byte | |||||
// 4c + lengthbyte and alt push options are not supported | |||||
FabienUnsubmitted Done Inline ActionsAFAICT that's not compliant with the spec Fabien: AFAICT that's not compliant with the spec | |||||
// Validate aliasLengthBytes | |||||
// Must be between 1 and 21 bytes inclusive | |||||
const isValidAliasLength = | |||||
typeof aliasLengthBytesDecimal === 'number' && | |||||
aliasLengthBytesDecimal <= 21 && | |||||
aliasLengthBytesDecimal >= 1; | |||||
if (!isValidAliasLength) { | |||||
FabienUnsubmitted Done Inline Actionsdito Fabien: dito | |||||
// If the registered alias is not of a valid length, | |||||
// this is not a valid alias registration | |||||
// Check the next output | |||||
continue; | |||||
} | } | ||||
// Check for valid alias length | // Remove aliasLengthBytesHex from the stack | ||||
const aliasLengthHex = outputScript.slice(12, 14); | stack = stack.slice(2); | ||||
aliasLength = parseInt(aliasLengthHex, 16); | |||||
// Get aliasHex from the stack | |||||
// Parse for the alias | const aliasHex = stack.slice(0, 2 * aliasLengthBytesDecimal); | ||||
const aliasHex = outputScript.slice(14, outputScript.length); | |||||
alias = getAliasFromHex(aliasHex); | // 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; | continue; | ||||
} | } | ||||
const validAliasLength = | // Remove aliasHex from the stack | ||||
aliasLength <= aliasConstants.maxLength && | stack = stack.slice(2 * aliasLengthBytesDecimal); | ||||
aliasHex.length === 2 * aliasLength; | |||||
if (!validAliasLength) { | // Get the length of address version byte + address | ||||
FabienUnsubmitted Done Inline Actionsit's type, not version Fabien: it's type, not version | |||||
return false; | // 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, | |||||
); | |||||
FabienUnsubmitted Not Done Inline ActionsHow do you deal with other push types ? Fabien: How do you deal with other push types ? | |||||
bytesofmanAuthorUnsubmitted Done Inline ActionsI'm not familiar with every possible push type here. It's easier to control for this by modifying the spec to only support the push type that uses the least data. In this case, no one would ever need to push with 4c or in some other way, so why not constrain available push type to the most efficient in the spec? Supporting 4c is simple enough, but I think I'm still overlooking other options. bytesofman: I'm not familiar with every possible push type here.
It's easier to control for this by… | |||||
// Remove addressLengthInBytesHex from the stack | |||||
stack = stack.slice(2); | |||||
// Get the address push | |||||
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) { | |||||
// 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) { | ||||
FabienUnsubmitted Not Done Inline ActionsWhat's the point of using an array if the size can only be 1 ? Fabien: What's the point of using an array if the size can only be 1 ? | |||||
bytesofmanAuthorUnsubmitted Done Inline ActionsThis approach was selected to handle the edge case of a tx with multiple OP_RETURNS, each a nominally valid alias registration. If I don't find this specific edge case, then such a tx could arbitrarily call the OP_RETURN output with the lowest index a valid alias registration. We could support this in the spec. However, I think it's better to not support, because
An alt approach would be to say in the spec, "alias registration txs must be at the OP_RETURN field with the lowest output index", and do away with this array. bytesofman: This approach was selected to handle the edge case of a tx with multiple OP_RETURNS, each a… | |||||
// A valid alias registration tx must have only one valid OP_RETURN | |||||
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 |
This variable is useless, just put the condition in the if()