Reject legacy addresses at CARROT heights, fix wallet B sync bug
After HF10 legacy CryptoNote addresses cannot be used — CARROT outputs built with legacy pubkeys are undetectable by the receiver's CARROT scanner because the CN view key differs from the CARROT viewIncomingKey. transfer() and sweep() now fail fast with a clear error when a SaLv... address is used post-HF10, using the network-aware hard fork table (not hardcoded heights).
This commit is contained in:
@@ -112,6 +112,7 @@ export function parseAddress(address) {
|
||||
result.paymentId = data.slice(KEY_SIZE * 2, KEY_SIZE * 2 + PAYMENT_ID_SIZE);
|
||||
}
|
||||
|
||||
result.isCarrot = result.format === 'carrot';
|
||||
result.valid = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
+2
-1
@@ -1102,7 +1102,8 @@ export class WalletSync {
|
||||
const rct = {
|
||||
type: txJson.rct_signatures?.type || 0,
|
||||
txnFee: this._safeBigInt(txJson.rct_signatures?.txnFee),
|
||||
// p_r is the ephemeral pubkey used for CARROT output scanning
|
||||
// p_r is the mask difference commitment in Salvium RCT (NOT the CARROT ephemeral pubkey D_e).
|
||||
// D_e is stored as txPubKey in tx_extra (tag 0x01).
|
||||
p_r: txJson.rct_signatures?.p_r ? hexToBytes(txJson.rct_signatures.p_r) : null,
|
||||
outPk: (txJson.rct_signatures?.outPk || []).map(pk => {
|
||||
if (typeof pk === 'string') return hexToBytes(pk);
|
||||
|
||||
@@ -272,6 +272,7 @@ export async function transfer({ wallet, daemon, destinations, options = {} }) {
|
||||
viewPublicKey: parsed.viewPublicKey,
|
||||
spendPublicKey: parsed.spendPublicKey,
|
||||
isSubaddress: parsed.type === 'subaddress',
|
||||
isCarrot: parsed.isCarrot,
|
||||
amount: typeof d.amount === 'bigint' ? d.amount : BigInt(d.amount)
|
||||
};
|
||||
});
|
||||
@@ -290,6 +291,20 @@ export async function transfer({ wallet, daemon, destinations, options = {} }) {
|
||||
blockWeightMedian: infoData?.block_weight_median || infoData?.block_weight_limit / 2 || 300000,
|
||||
};
|
||||
|
||||
// 3b. Reject legacy addresses at CARROT heights (fail fast before UTXO work).
|
||||
// After HF10, only CARROT addresses are valid. Legacy CryptoNote pubkeys differ
|
||||
// from CARROT pubkeys, so the receiver's scanner would never detect the output.
|
||||
if (isCarrotActive(currentHeight, options.network)) {
|
||||
for (const d of parsedDests) {
|
||||
if (d.isCarrot === false) {
|
||||
throw new Error(
|
||||
'Legacy address cannot be used at CARROT heights (post-HF10). ' +
|
||||
'Use a CARROT address (SC1...) instead.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Get spendable outputs — use HF-based asset type if not specified
|
||||
const assetType = assetTypeOpt || getActiveAssetType(currentHeight, options.network);
|
||||
const allOutputs = await wallet.storage.getOutputs({
|
||||
@@ -499,6 +514,14 @@ export async function sweep({ wallet, daemon, address, options = {} }) {
|
||||
blockWeightMedian: infoData?.block_weight_median || infoData?.block_weight_limit / 2 || 300000,
|
||||
};
|
||||
|
||||
// Reject legacy addresses at CARROT heights
|
||||
if (isCarrotActive(currentHeight, options.network) && parsed.isCarrot === false) {
|
||||
throw new Error(
|
||||
'Legacy address cannot be used at CARROT heights (post-HF10). ' +
|
||||
'Use a CARROT address (SC1...) instead.'
|
||||
);
|
||||
}
|
||||
|
||||
// Use HF-based asset type detection (like C++ wallet2)
|
||||
const assetType = assetTypeOpt || getActiveAssetType(currentHeight, options.network);
|
||||
|
||||
|
||||
@@ -390,8 +390,13 @@ async function phaseCN(walletA, walletB) {
|
||||
banner('PHASE 1: CN ERA (pre-CARROT, height < 1100)');
|
||||
await refreshAssetType();
|
||||
|
||||
const addrA = walletA.getLegacyAddress();
|
||||
const addrB = walletB.getLegacyAddress();
|
||||
// Post-HF10: CARROT outputs require CARROT addresses. Using legacy addresses
|
||||
// at CARROT heights causes a pubkey mismatch (legacy CN view key != CARROT view key),
|
||||
// making outputs undetectable by the receiver's CARROT scanner.
|
||||
const h = await getHeight(daemon);
|
||||
const postCarrot = h >= CARROT_FORK_HEIGHT;
|
||||
const addrA = postCarrot ? walletA.getCarrotAddress() : walletA.getLegacyAddress();
|
||||
const addrB = postCarrot ? walletB.getCarrotAddress() : walletB.getLegacyAddress();
|
||||
console.log(` A address: ${short(addrA)}`);
|
||||
console.log(` B address: ${short(addrB)}`);
|
||||
|
||||
|
||||
@@ -448,6 +448,114 @@ test('WALLET_TYPE constants exist', () => {
|
||||
assertEqual(WALLET_TYPE.WATCH, 'watch');
|
||||
});
|
||||
|
||||
// Legacy address rejection at CARROT heights
|
||||
console.log('\n--- Legacy Address Rejection ---');
|
||||
|
||||
import { transfer } from '../src/wallet/transfer.js';
|
||||
|
||||
// Mock daemon that returns a configurable height
|
||||
function mockDaemon(height) {
|
||||
return {
|
||||
getInfo: async () => ({
|
||||
success: true,
|
||||
result: { height, block_weight_median: 300000 }
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
test('transfer rejects legacy address after HF10 (testnet)', async () => {
|
||||
const wallet = Wallet.create('testnet');
|
||||
const legacyAddr = wallet.getLegacyAddress();
|
||||
const mockD = mockDaemon(1200); // post-CARROT (HF10 = height 1100 on testnet)
|
||||
|
||||
let threw = false;
|
||||
try {
|
||||
await transfer({
|
||||
wallet, daemon: mockD,
|
||||
destinations: [{ address: legacyAddr, amount: 100000000n }],
|
||||
options: { network: 'testnet', assetType: 'SAL1' }
|
||||
});
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
assert(e.message.includes('Legacy address cannot be used'), `Wrong error: ${e.message}`);
|
||||
}
|
||||
assert(threw, 'Should have thrown for legacy address at CARROT height');
|
||||
});
|
||||
|
||||
test('transfer accepts legacy address before HF10 (testnet)', async () => {
|
||||
const wallet = Wallet.create('testnet');
|
||||
const legacyAddr = wallet.getLegacyAddress();
|
||||
const mockD = mockDaemon(500); // pre-CARROT
|
||||
|
||||
let legacyRejected = false;
|
||||
try {
|
||||
await transfer({
|
||||
wallet, daemon: mockD,
|
||||
destinations: [{ address: legacyAddr, amount: 100000000n }],
|
||||
options: { network: 'testnet', assetType: 'SAL' }
|
||||
});
|
||||
} catch (e) {
|
||||
// Should fail for other reasons (no UTXOs), but NOT legacy address rejection
|
||||
if (e.message.includes('Legacy address cannot be used')) legacyRejected = true;
|
||||
}
|
||||
assert(!legacyRejected, 'Should NOT reject legacy address before HF10');
|
||||
});
|
||||
|
||||
test('transfer accepts CARROT address after HF10 (testnet)', async () => {
|
||||
const wallet = Wallet.create('testnet');
|
||||
const carrotAddr = wallet.getCarrotAddress();
|
||||
const mockD = mockDaemon(1200); // post-CARROT
|
||||
|
||||
let legacyRejected = false;
|
||||
try {
|
||||
await transfer({
|
||||
wallet, daemon: mockD,
|
||||
destinations: [{ address: carrotAddr, amount: 100000000n }],
|
||||
options: { network: 'testnet', assetType: 'SAL1' }
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.message.includes('Legacy address cannot be used')) legacyRejected = true;
|
||||
}
|
||||
assert(!legacyRejected, 'Should NOT reject CARROT address at CARROT height');
|
||||
});
|
||||
|
||||
test('transfer rejects legacy address after HF10 (mainnet)', async () => {
|
||||
const wallet = Wallet.create('mainnet');
|
||||
const legacyAddr = wallet.getLegacyAddress();
|
||||
const mockD = mockDaemon(340000); // post-CARROT (HF10 = 334750 on mainnet)
|
||||
|
||||
let threw = false;
|
||||
try {
|
||||
await transfer({
|
||||
wallet, daemon: mockD,
|
||||
destinations: [{ address: legacyAddr, amount: 100000000n }],
|
||||
options: { network: 'mainnet', assetType: 'SAL1' }
|
||||
});
|
||||
} catch (e) {
|
||||
threw = true;
|
||||
assert(e.message.includes('Legacy address cannot be used'), `Wrong error: ${e.message}`);
|
||||
}
|
||||
assert(threw, 'Should have thrown for legacy address at CARROT height (mainnet)');
|
||||
});
|
||||
|
||||
test('transfer accepts legacy address before HF10 (mainnet)', async () => {
|
||||
const wallet = Wallet.create('mainnet');
|
||||
const legacyAddr = wallet.getLegacyAddress();
|
||||
const mockD = mockDaemon(330000); // pre-CARROT on mainnet
|
||||
|
||||
let legacyRejected = false;
|
||||
try {
|
||||
await transfer({
|
||||
wallet, daemon: mockD,
|
||||
destinations: [{ address: legacyAddr, amount: 100000000n }],
|
||||
options: { network: 'mainnet', assetType: 'SAL1' }
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.message.includes('Legacy address cannot be used')) legacyRejected = true;
|
||||
}
|
||||
assert(!legacyRejected, 'Should NOT reject legacy address before HF10 (mainnet)');
|
||||
});
|
||||
|
||||
// Summary
|
||||
console.log(`\n--- Summary ---`);
|
||||
console.log(`Passed: ${passed}`);
|
||||
|
||||
Reference in New Issue
Block a user