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

feat(e-upgrade): add dynamic fees support [WIP] #871

Closed
wants to merge 2 commits into from
Closed
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
14 changes: 14 additions & 0 deletions src/fixtures/secp256k1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Credential } from '../serializable/fxs/secp256k1/credential';
import { OutputOwnersList } from '../serializable/fxs/secp256k1/outputOwnersList';
import { Signature } from '../serializable/fxs/secp256k1/signature';
import { concatBytes, hexToBuffer } from '../utils/buffer';
import { base58check } from '../utils/base58';
import { addresses, addressesBytes } from './common';
import {
bigIntPr,
Expand Down Expand Up @@ -105,3 +106,16 @@ export const signature2 = () =>
export const credentialBytes = () =>
concatBytes(bytesForInt(2), signatureBytes(), signature2Bytes());
export const credential = () => new Credential([signature(), signature2()]);

/**
* @see https://github.com/ava-labs/avalanchego/blob/master/utils/crypto/secp256k1/test_keys.go#L8
* @returns Returns 5 private keys
*/
export const testKeys = () =>
[
'24jUJ9vZexUM6expyMcT48LBx27k1m7xpraoV62oSQAHdziao5',
'2MMvUMsxx6zsHSNXJdFD8yc5XkancvwyKPwpw4xUK3TCGDuNBY',
'cxb7KpGWhDMALTjNNSJ7UQkkomPesyWAPUaWRGdyeBNzR6f35',
'ewoqjP7PxY4yr3iLTpLisriqt94hdyDFNgchSxGGztUrTXtNN',
'2RWLv6YVEXDiWLpaCbXhhqxtLbnFaKQsWPSSMSPhpWo47uJAeV',
].map(base58check.decode);
98 changes: 98 additions & 0 deletions src/vms/common/fees/calculator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type { Context } from '../../../vms/context';
import { testContext } from '../../../fixtures/context';
import { testKeys } from '../../../fixtures/secp256k1';
import { subnetValidator } from '../../../fixtures/pvm';
import {
Address,
BigIntPr,
Input,
Id,
Int,
OutputOwners,
TransferableInput,
TransferInput,
TransferOutput,
} from '../../../serializable';
import { BaseTx, TransferableOutput, UTXOID } from '../../../serializable/avax';
import { getPVMManager } from '../../../serializable/pvm/codec';
import {
AddSubnetValidatorTx,
StakeableLockOut,
} from '../../../serializable/pvm';
import { secp256k1 } from '../../../crypto';

import type { GasConfig } from './calculator';
import { calculateFees } from './calculator';
import { FeeDimensionWeights } from './dimensions';
import { meterTx } from './helpers';

describe('fee calculator', () => {
const TEST_GAS_CONFIG: GasConfig = {
gasCap: 100_000n,
gasPrice: 10n,
weights: FeeDimensionWeights,
};
// https://github.com/ava-labs/avalanchego/blob/13a3f103fd12df1e89a60c0c922b38e17872c6f6/vms/platformvm/txs/fee/calculator_test.go#L144C22-L144C28
it.skip('works for add subnet validator tx', () => {
const { baseTx, auth } = txCreationHelpers(testContext);
const unsignedTx = new AddSubnetValidatorTx(
baseTx,
subnetValidator(),
auth,
);
const complexity = meterTx(getPVMManager().getDefaultCodec(), unsignedTx);
const fee = calculateFees(complexity, TEST_GAS_CONFIG);
expect(fee).toEqual(29_110n); // TODO: Fix broken test...
});
});

/**
* @see https://github.com/ava-labs/avalanchego/blob/13a3f103fd12df1e89a60c0c922b38e17872c6f6/vms/platformvm/txs/fee/calculator_test.go#L916
* @param context
* @returns
*/
const txCreationHelpers = (context: Context) => {
const now = BigInt(new Date().getTime()) / 1000n;
// const empty = new Id(new Uint8Array(32)).toString();
const privateKey = testKeys()[0];
const publicKey = secp256k1.getPublicKey(privateKey);
// const feeTestSigners = testKeys;
const feeTestDefaultStakeWeight = 2024n;
const utxoId = new UTXOID(Id.fromString('txid'), new Int(2));
const assetId = Id.fromString(context.avaxAssetID);
const input = new TransferableInput(
utxoId,
assetId,
new TransferInput(new BigIntPr(5678n), new Input([new Int(0)])),
);
const output = new TransferableOutput(
assetId,
new TransferOutput(
new BigIntPr(1234n),
new OutputOwners(new BigIntPr(0n), new Int(1), [
Address.fromBytes(publicKey)[0],
]),
),
);
const baseTx = BaseTx.fromNative(
context.networkID,
context.pBlockchainID,
[output],
[input],
new Uint8Array(),
);
const stakes = new TransferableOutput(
assetId,
new StakeableLockOut(
new BigIntPr(now),
new TransferOutput(
new BigIntPr(feeTestDefaultStakeWeight),
new OutputOwners(new BigIntPr(0n), new Int(1), [
Address.fromBytes(publicKey)[0],
]),
),
),
);
const auth = new Input([new Int(0), new Int(1)]);
return { baseTx, stakes, auth };
};
20 changes: 20 additions & 0 deletions src/vms/common/fees/calculator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Dimensions } from './dimensions';
import { toGas } from './dimensions';

export type GasConfig = {
gasPrice: bigint;
gasCap: bigint;
weights: Dimensions;
};

export const calculateFees = (
complexities: Dimensions,
gasConfig: GasConfig,
): bigint => {
const gas = toGas(complexities, gasConfig.weights);
if (gas > gasConfig.gasCap) {
throw new Error('gas exceeds gasCap');
}
const fee = gas * gasConfig.gasPrice;
return fee;
};
9 changes: 9 additions & 0 deletions src/vms/common/fees/cost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { TransferInput, TransferableInput } from '../../../serializable';

/**
* @see https://github.com/ava-labs/avalanchego/blob/master/vms/secp256k1fx/input.go#L14
*/
export const COST_PER_SIGNATURE = 1000n;

export const getCost = (input: TransferInput | TransferableInput): bigint =>
BigInt(input.sigIndicies().length) * COST_PER_SIGNATURE;
60 changes: 60 additions & 0 deletions src/vms/common/fees/dimensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* @see https://github.com/ava-labs/avalanchego/pull/2682/files#diff-405f8744e9d1e46ef736babec06fb58d04b5d413d90eb0a28a40787d6fe85c79R69
*/
const FEE_GAS_FACTOR = 10n;

/**
* @see https://github.com/ava-labs/avalanchego/blob/13a3f103fd12df1e89a60c0c922b38e17872c6f6/vms/components/fee/dimensions.go#L10-L16
*/
export enum FeeDimensions {
Bandwidth = 0,
DBRead = 1,
DBWrite = 2,
Compute = 3,
}

export type Dimensions = Record<FeeDimensions, bigint>;

export const emptyDimensions = (): Dimensions => ({
[FeeDimensions.Bandwidth]: BigInt(0),
[FeeDimensions.DBRead]: BigInt(0),
[FeeDimensions.DBWrite]: BigInt(0),
[FeeDimensions.Compute]: BigInt(0),
});

/**
* @see https://github.com/ava-labs/avalanchego/blob/13a3f103fd12df1e89a60c0c922b38e17872c6f6/vms/platformvm/txs/fee/dynamic_config.go#L24
*/
export const FeeDimensionWeights: Dimensions = {
[FeeDimensions.Bandwidth]: BigInt(1),
[FeeDimensions.DBRead]: BigInt(1),
[FeeDimensions.DBWrite]: BigInt(1),
[FeeDimensions.Compute]: BigInt(1),
};

export const toGas = (
complexities: Dimensions,
weights: Dimensions,
): bigint => {
return (
Object.entries(complexities).reduce(
(agg, [feeDimension, complexity]) =>
agg + complexity * weights[feeDimension],
0n,
) / FEE_GAS_FACTOR
);
};

export const addDimensions = (
left: Dimensions,
right: Dimensions,
): Dimensions => ({
[FeeDimensions.Bandwidth]:
left[FeeDimensions.Bandwidth] + right[FeeDimensions.Bandwidth],
[FeeDimensions.DBRead]:
left[FeeDimensions.DBRead] + right[FeeDimensions.DBRead],
[FeeDimensions.DBWrite]:
left[FeeDimensions.DBWrite] + right[FeeDimensions.DBWrite],
[FeeDimensions.Compute]:
left[FeeDimensions.Compute] + right[FeeDimensions.Compute],
});
153 changes: 153 additions & 0 deletions src/vms/common/fees/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import type {
TransferableInput,
TransferableOutput,
} from '../../../serializable';
import { Credential, avaxSerial } from '../../../serializable';
import type { AvaxTx } from '../../../serializable/avax';
import type { Codec } from '../../../serializable/codec';
import {
isAddSubnetValidatorTx,
isRemoveSubnetValidatorTx,
} from '../../../serializable/pvm';
import { emptySignature } from '../../../constants/zeroValue';
import {
getTransferableInputsByTx,
getTransferableOutputsByTx,
} from '../../../utils';

import {
FeeDimensions,
addDimensions,
emptyDimensions,
type Dimensions,
} from './dimensions';
import { getCost } from './cost';

/**
* @see https://github.com/ava-labs/avalanchego/blob/13a3f103fd12df1e89a60c0c922b38e17872c6f6/vms/platformvm/txs/fee/dynamic_calculator.go#L19
*/
const STAKER_LOOKUP_COST = 1000n; // equal to secp256k1fx.CostPerSignature;

/**
* Codec.VersionSize -> wrappers.ShortLen -> 2
* @see https://github.com/ava-labs/avalanchego/blob/master/codec/manager.go#L16
*/
const CODEC_VERSION_SIZE = 2n;
const WRAPPERS_INT_LEN = 4n;

/**
* @see https://github.com/ava-labs/avalanchego/blob/13a3f103fd12df1e89a60c0c922b38e17872c6f6/vms/components/fee/helpers.go#L17
* @param codec
* @param input
* @returns
*/
export const meterInput = (
codec: Codec,
input: TransferableInput,
): Dimensions => {
const size = BigInt(input.toBytes(codec).length) + CODEC_VERSION_SIZE;

return {
[FeeDimensions.Bandwidth]: size - CODEC_VERSION_SIZE,
[FeeDimensions.Compute]: getCost(input),
[FeeDimensions.DBRead]: size,
[FeeDimensions.DBWrite]: size,
};
};

/**
* @see https://github.com/ava-labs/avalanchego/blob/13a3f103fd12df1e89a60c0c922b38e17872c6f6/vms/components/fee/helpers.go#L37
* @param codec
* @param output
* @returns
*/
export const meterOutput = (
codec: Codec,
output: TransferableOutput,
): Dimensions => {
const size = BigInt(output.toBytes(codec).length) + CODEC_VERSION_SIZE;

return {
...emptyDimensions(),
[FeeDimensions.Bandwidth]: size - CODEC_VERSION_SIZE,
[FeeDimensions.DBWrite]: size,
};
};

/**
* @see https://github.com/ava-labs/avalanchego/blob/13a3f103fd12df1e89a60c0c922b38e17872c6f6/vms/components/fee/helpers.go#L50
* @param codec
* @param signatureCount
* @returns
*/
export const meterCredential = (
codec: Codec,
signatureCount: number,
): Dimensions => {
const signatures = new Array(signatureCount).fill(emptySignature);
const credential = new Credential(signatures);
const size = BigInt(credential.toBytes(codec).length) + CODEC_VERSION_SIZE;

return {
...emptyDimensions(),
[FeeDimensions.Bandwidth]: size - WRAPPERS_INT_LEN - CODEC_VERSION_SIZE,
};
};

/**
* @see https://github.com/ava-labs/avalanchego/blob/13a3f103fd12df1e89a60c0c922b38e17872c6f6/vms/platformvm/txs/fee/dynamic_calculator.go#L195
* @param codec
* @param tx
* @returns
*/
export const meterTx = (codec: Codec, tx: AvaxTx): Dimensions => {
let dimensions = emptyDimensions();
const size = BigInt(tx.toBytes(codec).length) + CODEC_VERSION_SIZE;
console.log({ size });
dimensions[FeeDimensions.Bandwidth] = size;

// Credentials
console.log({ tx, sigIndices: tx.getSigIndices() });
for (const credential of tx.getSigIndices()) {
const credentialDimensions = meterCredential(codec, credential.length);
console.log({ credential, credentialDimensions });
dimensions = addDimensions(dimensions, credentialDimensions);
}
dimensions[FeeDimensions.Bandwidth] += WRAPPERS_INT_LEN + CODEC_VERSION_SIZE;

// Inputs
const inputs = getTransferableInputsByTx(tx);
for (const input of inputs) {
const inputDimensions = meterInput(codec, input);
inputDimensions[FeeDimensions.Bandwidth] = 0n; // inputs bandwidth is already accounted for above, so we zero it
dimensions = addDimensions(dimensions, inputDimensions);
}

// Outputs
const outputs = getTransferableOutputsByTx(tx);
for (const output of outputs.filter(avaxSerial.isTransferableOutput)) {
const outputDimensions = meterOutput(codec, output);
outputDimensions[FeeDimensions.Bandwidth] = 0n; // output bandwidth is already accounted for above, so we zero it
dimensions = addDimensions(dimensions, outputDimensions);
}

// Specific costs per Tx type
const txSpecific = getTxSpecificComplexity(tx);

return addDimensions(dimensions, txSpecific);
};

/**
* @see https://github.com/ava-labs/avalanchego/blob/13a3f103fd12df1e89a60c0c922b38e17872c6f6/vms/platformvm/txs/fee/dynamic_calculator.go#L50
* @param tx
* @returns
*/
export const getTxSpecificComplexity = (tx: AvaxTx): Dimensions => {
if (isAddSubnetValidatorTx(tx) || isRemoveSubnetValidatorTx(tx)) {
return {
...emptyDimensions(),
[FeeDimensions.Compute]: STAKER_LOOKUP_COST,
};
}
return emptyDimensions();
};
Loading