From ef0566643361e8afd97c7785a81ec3ca90268a1f Mon Sep 17 00:00:00 2001 From: Samuel Golland Date: Mon, 8 Jul 2024 13:29:39 -0400 Subject: [PATCH 1/2] feat: meter input output credential --- src/vms/common/fees/cost.ts | 9 ++++ src/vms/common/fees/dimensions.ts | 39 ++++++++++++++++ src/vms/common/fees/helpers.ts | 74 +++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 src/vms/common/fees/cost.ts create mode 100644 src/vms/common/fees/dimensions.ts create mode 100644 src/vms/common/fees/helpers.ts diff --git a/src/vms/common/fees/cost.ts b/src/vms/common/fees/cost.ts new file mode 100644 index 000000000..c56c9347b --- /dev/null +++ b/src/vms/common/fees/cost.ts @@ -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; diff --git a/src/vms/common/fees/dimensions.ts b/src/vms/common/fees/dimensions.ts new file mode 100644 index 000000000..e2818f09c --- /dev/null +++ b/src/vms/common/fees/dimensions.ts @@ -0,0 +1,39 @@ +/** + * @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; + +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, + ); +}; diff --git a/src/vms/common/fees/helpers.ts b/src/vms/common/fees/helpers.ts new file mode 100644 index 000000000..f9b97c445 --- /dev/null +++ b/src/vms/common/fees/helpers.ts @@ -0,0 +1,74 @@ +import type { TransferableInput, TransferableOutput } from 'serializable'; +import type { Dimensions } from './dimensions'; +import { FeeDimensions, emptyDimensions } from './dimensions'; +import { getCost } from './cost'; +import type { Codec } from 'serializable/codec'; +import { Credential } from 'serializable'; +import { emptySignature } from 'constants/zeroValue'; + +/** + * @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); + + return { + // https://github.com/ava-labs/avalanchego/blob/master/codec/manager.go#L16 + // Subtracting 2 because Codec.VersionSize -> wrappers.ShortLen -> 2 + [FeeDimensions.Bandwidth]: size - 2n, + [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); + + return { + ...emptyDimensions(), + // https://github.com/ava-labs/avalanchego/blob/master/codec/manager.go#L16 + // Subtracting 2 because Codec.VersionSize -> wrappers.ShortLen -> 2 + [FeeDimensions.Bandwidth]: size - 2n, + [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 credential = new Credential( + new Array(signatureCount).map(() => emptySignature), + ); + const size = BigInt(credential.toBytes(codec).length); + + return { + ...emptyDimensions(), + // https://github.com/ava-labs/avalanchego/blob/master/codec/manager.go#L16 + // Subtracting 4 because wrappers.IntLen -> 4 + // Subtracting 2 because Codec.VersionSize -> wrappers.ShortLen -> 2 + [FeeDimensions.Bandwidth]: size - 4n - 2n, + }; +}; From b7b2db075e24b23ebc018a9974457ec1b3dd53cb Mon Sep 17 00:00:00 2001 From: Samuel Golland Date: Wed, 10 Jul 2024 10:29:47 -0400 Subject: [PATCH 2/2] chore: add broken test --- src/fixtures/secp256k1.ts | 14 +++ src/vms/common/fees/calculator.test.ts | 98 ++++++++++++++++++++ src/vms/common/fees/calculator.ts | 20 ++++ src/vms/common/fees/cost.ts | 2 +- src/vms/common/fees/dimensions.ts | 29 +++++- src/vms/common/fees/helpers.ts | 123 ++++++++++++++++++++----- 6 files changed, 259 insertions(+), 27 deletions(-) create mode 100644 src/vms/common/fees/calculator.test.ts create mode 100644 src/vms/common/fees/calculator.ts diff --git a/src/fixtures/secp256k1.ts b/src/fixtures/secp256k1.ts index 3a3d66851..f887b40d4 100644 --- a/src/fixtures/secp256k1.ts +++ b/src/fixtures/secp256k1.ts @@ -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, @@ -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); diff --git a/src/vms/common/fees/calculator.test.ts b/src/vms/common/fees/calculator.test.ts new file mode 100644 index 000000000..fb85959da --- /dev/null +++ b/src/vms/common/fees/calculator.test.ts @@ -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 }; +}; diff --git a/src/vms/common/fees/calculator.ts b/src/vms/common/fees/calculator.ts new file mode 100644 index 000000000..3b6fadf46 --- /dev/null +++ b/src/vms/common/fees/calculator.ts @@ -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; +}; diff --git a/src/vms/common/fees/cost.ts b/src/vms/common/fees/cost.ts index c56c9347b..dc642105d 100644 --- a/src/vms/common/fees/cost.ts +++ b/src/vms/common/fees/cost.ts @@ -1,4 +1,4 @@ -import type { TransferInput, TransferableInput } from 'serializable'; +import type { TransferInput, TransferableInput } from '../../../serializable'; /** * @see https://github.com/ava-labs/avalanchego/blob/master/vms/secp256k1fx/input.go#L14 diff --git a/src/vms/common/fees/dimensions.ts b/src/vms/common/fees/dimensions.ts index e2818f09c..7dcb05bf9 100644 --- a/src/vms/common/fees/dimensions.ts +++ b/src/vms/common/fees/dimensions.ts @@ -1,3 +1,8 @@ +/** + * @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 */ @@ -31,9 +36,25 @@ export const toGas = ( complexities: Dimensions, weights: Dimensions, ): bigint => { - return Object.entries(complexities).reduce( - (agg, [feeDimension, complexity]) => - agg + complexity * weights[feeDimension], - 0n, + 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], +}); diff --git a/src/vms/common/fees/helpers.ts b/src/vms/common/fees/helpers.ts index f9b97c445..bc235b9de 100644 --- a/src/vms/common/fees/helpers.ts +++ b/src/vms/common/fees/helpers.ts @@ -1,10 +1,39 @@ -import type { TransferableInput, TransferableOutput } from 'serializable'; -import type { Dimensions } from './dimensions'; -import { FeeDimensions, emptyDimensions } from './dimensions'; +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'; -import type { Codec } from 'serializable/codec'; -import { Credential } from 'serializable'; -import { emptySignature } from 'constants/zeroValue'; + +/** + * @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 @@ -16,12 +45,10 @@ export const meterInput = ( codec: Codec, input: TransferableInput, ): Dimensions => { - const size = BigInt(input.toBytes(codec).length); + const size = BigInt(input.toBytes(codec).length) + CODEC_VERSION_SIZE; return { - // https://github.com/ava-labs/avalanchego/blob/master/codec/manager.go#L16 - // Subtracting 2 because Codec.VersionSize -> wrappers.ShortLen -> 2 - [FeeDimensions.Bandwidth]: size - 2n, + [FeeDimensions.Bandwidth]: size - CODEC_VERSION_SIZE, [FeeDimensions.Compute]: getCost(input), [FeeDimensions.DBRead]: size, [FeeDimensions.DBWrite]: size, @@ -38,13 +65,11 @@ export const meterOutput = ( codec: Codec, output: TransferableOutput, ): Dimensions => { - const size = BigInt(output.toBytes(codec).length); + const size = BigInt(output.toBytes(codec).length) + CODEC_VERSION_SIZE; return { ...emptyDimensions(), - // https://github.com/ava-labs/avalanchego/blob/master/codec/manager.go#L16 - // Subtracting 2 because Codec.VersionSize -> wrappers.ShortLen -> 2 - [FeeDimensions.Bandwidth]: size - 2n, + [FeeDimensions.Bandwidth]: size - CODEC_VERSION_SIZE, [FeeDimensions.DBWrite]: size, }; }; @@ -59,16 +84,70 @@ export const meterCredential = ( codec: Codec, signatureCount: number, ): Dimensions => { - const credential = new Credential( - new Array(signatureCount).map(() => emptySignature), - ); - const size = BigInt(credential.toBytes(codec).length); + const signatures = new Array(signatureCount).fill(emptySignature); + const credential = new Credential(signatures); + const size = BigInt(credential.toBytes(codec).length) + CODEC_VERSION_SIZE; return { ...emptyDimensions(), - // https://github.com/ava-labs/avalanchego/blob/master/codec/manager.go#L16 - // Subtracting 4 because wrappers.IntLen -> 4 - // Subtracting 2 because Codec.VersionSize -> wrappers.ShortLen -> 2 - [FeeDimensions.Bandwidth]: size - 4n - 2n, + [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(); +};