Skip to content

Commit

Permalink
Implement Stake Pre-Splitting (#444)
Browse files Browse the repository at this point in the history
* add: Stake Splitting for improved staking efficiency

* fix: use TxBuilder's `valueOut` instead of the user's `value` input
This was causing change to be computed incorrectly if the actual `valueOut` differed from the user's `value` due to automated differences (i.e: Stake Split remainders)

* Prettier

* refactor: add review suggestion

* fix: eqeqeq linting error

* fix: avoid delegating dust

* tests: add Pre-Split + Dust Change testing

* Add review suggestion

* Add review suggestion

* Apply patch

---------

Co-authored-by: Alessandro Rezzi <[email protected]>
  • Loading branch information
JSKitty and panleone authored Dec 5, 2024
1 parent 92be245 commit 06eef78
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 21 deletions.
6 changes: 4 additions & 2 deletions chain_params.prod.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
"proposalFeeConfirmRequirement": 6,
"maxPaymentCycles": 6,
"maxPayment": 43200000000000,
"defaultColdStakingAddress": "SdgQDpS8jDRJDX8yK8m9KnTMarsE84zdsy"
"defaultColdStakingAddress": "SdgQDpS8jDRJDX8yK8m9KnTMarsE84zdsy",
"stakeSplitTarget": 50000000000
},
"testnet": {
"name": "testnet",
Expand Down Expand Up @@ -64,6 +65,7 @@
"proposalFeeConfirmRequirement": 3,
"maxPaymentCycles": 20,
"maxPayment": 144000000000,
"defaultColdStakingAddress": "WmNziUEPyhnUkiVdfsiNX93H6rSJnios44"
"defaultColdStakingAddress": "WmNziUEPyhnUkiVdfsiNX93H6rSJnios44",
"stakeSplitTarget": 50000000000
}
}
6 changes: 4 additions & 2 deletions chain_params.test.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"proposalFeeConfirmRequirement": 6,
"maxPaymentCycles": 6,
"maxPayment": 43200000000000,
"defaultColdStakingAddress": "SdgQDpS8jDRJDX8yK8m9KnTMarsE84zdsy"
"defaultColdStakingAddress": "SdgQDpS8jDRJDX8yK8m9KnTMarsE84zdsy",
"stakeSplitTarget": 50000000000
},
"testnet": {
"name": "testnet",
Expand Down Expand Up @@ -60,6 +61,7 @@
"proposalFeeConfirmRequirement": 3,
"maxPaymentCycles": 20,
"maxPayment": 144000000000,
"defaultColdStakingAddress": "WmNziUEPyhnUkiVdfsiNX93H6rSJnios44"
"defaultColdStakingAddress": "WmNziUEPyhnUkiVdfsiNX93H6rSJnios44",
"stakeSplitTarget": 50000000000
}
}
28 changes: 22 additions & 6 deletions scripts/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -1164,11 +1164,26 @@ export class Wallet {
// Add primary output
if (isDelegation) {
if (!returnAddress) returnAddress = this.getNewChangeAddress();
transactionBuilder.addColdStakeOutput({
address: returnAddress,
addressColdStake: address,
value,
});
// The per-output target for maximum staking efficiency
const nTarget = cChainParams.current.stakeSplitTarget;
// Generate optimal staking outputs
if (value < COIN) {
throw new Error('below consensus');
} else if (value < nTarget) {
transactionBuilder.addColdStakeOutput({
address: returnAddress,
addressColdStake: address,
value,
});
} else {
for (let i = 0; i < Math.floor(value / nTarget); i++) {
transactionBuilder.addColdStakeOutput({
address: returnAddress,
addressColdStake: address,
value: i === 0 ? nTarget + (value % nTarget) : nTarget,
});
}
}
} else if (isProposal) {
transactionBuilder.addProposalOutput({
hash: address,
Expand Down Expand Up @@ -1198,7 +1213,8 @@ export class Wallet {
}

const fee = transactionBuilder.getFee();
const changeValue = transactionBuilder.valueIn - value - fee;
const changeValue =
transactionBuilder.valueIn - transactionBuilder.valueOut - fee;
if (changeValue < 0) {
if (!subtractFeeFromAmt) {
throw new Error('Not enough balance');
Expand Down
32 changes: 21 additions & 11 deletions tests/unit/wallet/transactions.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {

import 'fake-indexeddb/auto';
import { TransactionBuilder } from '../../../scripts/transaction_builder.js';
import { cChainParams } from '../../../scripts/chain_params.js';

vi.stubGlobal('localStorage', { length: 0 });
vi.mock('../../../scripts/global.js');
Expand Down Expand Up @@ -169,9 +170,11 @@ describe('Wallet transaction tests', () => {
});

it('Creates a cold stake tx correctly', async () => {
// Delegate 5250 PIV to test Stake Pre-Splitting
const value = 5250 * 10 ** 8;
const tx = wallet.createTransaction(
'SR3L4TFUKKGNsnv2Q4hWTuET2a4vHpm1b9',
0.05 * 10 ** 8,
value,
{ isDelegation: true }
);
expect(tx.version).toBe(1);
Expand All @@ -184,26 +187,32 @@ describe('Wallet transaction tests', () => {
scriptSig: '76a914f49b25384b79685227be5418f779b98a6be4c73888ac', // Script sig must be the UTXO script since it's not signed
})
);
expect(tx.vout[1]).toStrictEqual(
new CTxOut({
script: '76a914f49b25384b79685227be5418f779b98a6be4c73888ac',
value: 4997470,
})
);
// The 'after split' output
expect(tx.vout[0]).toStrictEqual(
new CTxOut({
script: '76a97b63d114291a25b5b4d1802e0611e9bf724a1e57d9210e826714f49b25384b79685227be5418f779b98a6be4c7386888ac',
value: 5000000,
value:
cChainParams.current.stakeSplitTarget +
(value % cChainParams.current.stakeSplitTarget),
})
);
// The split outputs (depending on chainparam 'stakeSplitTarget')
for (const cOut of tx.vout.slice(1, tx.vout.length - 1)) {
expect(cOut).toStrictEqual(
new CTxOut({
script: '76a97b63d114291a25b5b4d1802e0611e9bf724a1e57d9210e826714f49b25384b79685227be5418f779b98a6be4c7386888ac',
value: cChainParams.current.stakeSplitTarget,
})
);
}
await checkFees(wallet, tx, MIN_FEE_PER_BYTE);
});

it('creates a tx with max balance', async () => {
it('Creates a tx with max balance', async () => {
const tx = wallet.createTransaction(
'SR3L4TFUKKGNsnv2Q4hWTuET2a4vHpm1b9',
legacyMainnetInitialBalance(),
{ isDelegation: true }
{ isDelegation: false }
);
expect(tx.version).toBe(1);
expect(tx.vin).toHaveLength(2);
Expand All @@ -218,9 +227,10 @@ describe('Wallet transaction tests', () => {
);
expect(tx.vout).toHaveLength(1);
const fees = await checkFees(wallet, tx, MIN_FEE_PER_BYTE);
// The 'after split' output
expect(tx.vout[0]).toStrictEqual(
new CTxOut({
script: '76a97b63d114291a25b5b4d1802e0611e9bf724a1e57d9210e826714f49b25384b79685227be5418f779b98a6be4c7386888ac',
script: '76a914291a25b5b4d1802e0611e9bf724a1e57d9210e8288ac',
value: legacyMainnetInitialBalance() - fees,
})
);
Expand Down

0 comments on commit 06eef78

Please sign in to comment.