From 047d493084c9e5cad6ecf98ea9e4ee3e19b4c460 Mon Sep 17 00:00:00 2001 From: Dhruba Basu <7675102+dhrubabasu@users.noreply.github.com> Date: Tue, 31 Oct 2023 18:14:14 -0400 Subject: [PATCH] Add `BaseTx` support to platformvm (#2232) Signed-off-by: Dhruba Basu <7675102+dhrubabasu@users.noreply.github.com> --- vms/platformvm/metrics/tx_metrics.go | 9 +- vms/platformvm/txs/base_tx.go | 6 + vms/platformvm/txs/base_tx_test.go | 450 ++++++++++++++++-- vms/platformvm/txs/builder/builder.go | 51 ++ vms/platformvm/txs/builder/mock_builder.go | 16 + vms/platformvm/txs/codec.go | 6 +- .../txs/executor/atomic_tx_executor.go | 4 + .../txs/executor/proposal_tx_executor.go | 4 + .../txs/executor/standard_tx_executor.go | 31 ++ .../txs/executor/tx_mempool_verifier.go | 4 + vms/platformvm/txs/mempool/issuer.go | 5 + vms/platformvm/txs/mempool/remover.go | 5 + vms/platformvm/txs/visitor.go | 1 + vms/platformvm/vm_test.go | 75 +++ wallet/chain/p/backend_visitor.go | 4 + wallet/chain/p/signer_visitor.go | 8 + 16 files changed, 641 insertions(+), 38 deletions(-) diff --git a/vms/platformvm/metrics/tx_metrics.go b/vms/platformvm/metrics/tx_metrics.go index 17d6a090957b..9ed07bce7ec9 100644 --- a/vms/platformvm/metrics/tx_metrics.go +++ b/vms/platformvm/metrics/tx_metrics.go @@ -28,7 +28,8 @@ type txMetrics struct { numTransformSubnetTxs, numAddPermissionlessValidatorTxs, numAddPermissionlessDelegatorTxs, - numTransferSubnetOwnershipTxs prometheus.Counter + numTransferSubnetOwnershipTxs, + numBaseTxs prometheus.Counter } func newTxMetrics( @@ -51,6 +52,7 @@ func newTxMetrics( numAddPermissionlessValidatorTxs: newTxMetric(namespace, "add_permissionless_validator", registerer, &errs), numAddPermissionlessDelegatorTxs: newTxMetric(namespace, "add_permissionless_delegator", registerer, &errs), numTransferSubnetOwnershipTxs: newTxMetric(namespace, "transfer_subnet_ownership", registerer, &errs), + numBaseTxs: newTxMetric(namespace, "base", registerer, &errs), } return m, errs.Err } @@ -139,3 +141,8 @@ func (m *txMetrics) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) er m.numTransferSubnetOwnershipTxs.Inc() return nil } + +func (m *txMetrics) BaseTx(*txs.BaseTx) error { + m.numBaseTxs.Inc() + return nil +} diff --git a/vms/platformvm/txs/base_tx.go b/vms/platformvm/txs/base_tx.go index 2aa95f56cc68..5ffb308fe425 100644 --- a/vms/platformvm/txs/base_tx.go +++ b/vms/platformvm/txs/base_tx.go @@ -16,6 +16,8 @@ import ( ) var ( + _ UnsignedTx = (*BaseTx)(nil) + ErrNilTx = errors.New("tx is nil") errOutputsNotSorted = errors.New("outputs not sorted") @@ -96,3 +98,7 @@ func (tx *BaseTx) SyntacticVerify(ctx *snow.Context) error { return nil } } + +func (tx *BaseTx) Visit(visitor Visitor) error { + return visitor.BaseTx(tx) +} diff --git a/vms/platformvm/txs/base_tx_test.go b/vms/platformvm/txs/base_tx_test.go index 073b27f25056..c6cba1570312 100644 --- a/vms/platformvm/txs/base_tx_test.go +++ b/vms/platformvm/txs/base_tx_test.go @@ -10,65 +10,443 @@ import ( "github.com/stretchr/testify/require" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/utils" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/units" "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/avalanchego/vms/types" ) -func TestBaseTxMarshalJSON(t *testing.T) { +func TestBaseTxSerialization(t *testing.T) { require := require.New(t) - blockchainID := ids.ID{1} - utxoTxID := ids.ID{2} - assetID := ids.ID{3} - fxID := ids.ID{4} - tx := &BaseTx{BaseTx: avax.BaseTx{ - BlockchainID: blockchainID, - NetworkID: 4, - Ins: []*avax.TransferableInput{ - { - FxID: fxID, - UTXOID: avax.UTXOID{TxID: utxoTxID, OutputIndex: 5}, - Asset: avax.Asset{ID: assetID}, - In: &avax.TestTransferable{Val: 100}, + addr := ids.ShortID{ + 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0x44, 0x55, 0x66, 0x77, + } + + avaxAssetID, err := ids.FromString("FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z") + require.NoError(err) + + customAssetID := ids.ID{ + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + } + + txID := ids.ID{ + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + } + + simpleBaseTx := &BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: constants.MainnetID, + BlockchainID: constants.PlatformChainID, + Outs: []*avax.TransferableOutput{}, + Ins: []*avax.TransferableInput{ + { + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: 1, + }, + Asset: avax.Asset{ + ID: avaxAssetID, + }, + In: &secp256k1fx.TransferInput{ + Amt: units.MilliAvax, + Input: secp256k1fx.Input{ + SigIndices: []uint32{5}, + }, + }, + }, }, + Memo: types.JSONByteSlice{}, }, - Outs: []*avax.TransferableOutput{ - { - FxID: fxID, - Asset: avax.Asset{ID: assetID}, - Out: &avax.TestTransferable{Val: 100}, + } + require.NoError(simpleBaseTx.SyntacticVerify(&snow.Context{ + NetworkID: 1, + ChainID: constants.PlatformChainID, + AVAXAssetID: avaxAssetID, + })) + + expectedUnsignedSimpleBaseTxBytes := []byte{ + // Codec version + 0x00, 0x00, + // BaseTx Type ID + 0x00, 0x00, 0x00, 0x22, + // Mainnet network ID + 0x00, 0x00, 0x00, 0x01, + // P-chain blockchain ID + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Number of outputs + 0x00, 0x00, 0x00, 0x00, + // Number of inputs + 0x00, 0x00, 0x00, 0x01, + // Inputs[0] + // TxID + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + // Tx output index + 0x00, 0x00, 0x00, 0x01, + // Mainnet AVAX assetID + 0x21, 0xe6, 0x73, 0x17, 0xcb, 0xc4, 0xbe, 0x2a, + 0xeb, 0x00, 0x67, 0x7a, 0xd6, 0x46, 0x27, 0x78, + 0xa8, 0xf5, 0x22, 0x74, 0xb9, 0xd6, 0x05, 0xdf, + 0x25, 0x91, 0xb2, 0x30, 0x27, 0xa8, 0x7d, 0xff, + // secp256k1fx transfer input type ID + 0x00, 0x00, 0x00, 0x05, + // input amount = 1 MilliAvax + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x42, 0x40, + // number of signatures needed in input + 0x00, 0x00, 0x00, 0x01, + // index of signer + 0x00, 0x00, 0x00, 0x05, + // length of memo + 0x00, 0x00, 0x00, 0x00, + } + var unsignedSimpleBaseTx UnsignedTx = simpleBaseTx + unsignedSimpleBaseTxBytes, err := Codec.Marshal(Version, &unsignedSimpleBaseTx) + require.NoError(err) + require.Equal(expectedUnsignedSimpleBaseTxBytes, unsignedSimpleBaseTxBytes) + + complexBaseTx := &BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: constants.MainnetID, + BlockchainID: constants.PlatformChainID, + Outs: []*avax.TransferableOutput{ + { + Asset: avax.Asset{ + ID: avaxAssetID, + }, + Out: &stakeable.LockOut{ + Locktime: 87654321, + TransferableOut: &secp256k1fx.TransferOutput{ + Amt: 1, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 12345678, + Threshold: 0, + Addrs: []ids.ShortID{}, + }, + }, + }, + }, + { + Asset: avax.Asset{ + ID: customAssetID, + }, + Out: &stakeable.LockOut{ + Locktime: 876543210, + TransferableOut: &secp256k1fx.TransferOutput{ + Amt: 0xffffffffffffffff, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{ + addr, + }, + }, + }, + }, + }, + }, + Ins: []*avax.TransferableInput{ + { + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: 1, + }, + Asset: avax.Asset{ + ID: avaxAssetID, + }, + In: &secp256k1fx.TransferInput{ + Amt: units.Avax, + Input: secp256k1fx.Input{ + SigIndices: []uint32{2, 5}, + }, + }, + }, + { + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: 2, + }, + Asset: avax.Asset{ + ID: customAssetID, + }, + In: &stakeable.LockIn{ + Locktime: 876543210, + TransferableIn: &secp256k1fx.TransferInput{ + Amt: 0xefffffffffffffff, + Input: secp256k1fx.Input{ + SigIndices: []uint32{0}, + }, + }, + }, + }, + { + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: 3, + }, + Asset: avax.Asset{ + ID: customAssetID, + }, + In: &secp256k1fx.TransferInput{ + Amt: 0x1000000000000000, + Input: secp256k1fx.Input{ + SigIndices: []uint32{}, + }, + }, + }, }, + Memo: types.JSONByteSlice("😅\nwell that's\x01\x23\x45!"), }, - Memo: []byte{1, 2, 3}, - }} + } + avax.SortTransferableOutputs(complexBaseTx.Outs, Codec) + utils.Sort(complexBaseTx.Ins) + require.NoError(complexBaseTx.SyntacticVerify(&snow.Context{ + NetworkID: 1, + ChainID: constants.PlatformChainID, + AVAXAssetID: avaxAssetID, + })) - txJSONBytes, err := json.MarshalIndent(tx, "", "\t") + expectedUnsignedComplexBaseTxBytes := []byte{ + // Codec version + 0x00, 0x00, + // BaseTx Type ID + 0x00, 0x00, 0x00, 0x22, + // Mainnet network ID + 0x00, 0x00, 0x00, 0x01, + // P-chain blockchain ID + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Number of outputs + 0x00, 0x00, 0x00, 0x02, + // Outputs[0] + // Mainnet AVAX assetID + 0x21, 0xe6, 0x73, 0x17, 0xcb, 0xc4, 0xbe, 0x2a, + 0xeb, 0x00, 0x67, 0x7a, 0xd6, 0x46, 0x27, 0x78, + 0xa8, 0xf5, 0x22, 0x74, 0xb9, 0xd6, 0x05, 0xdf, + 0x25, 0x91, 0xb2, 0x30, 0x27, 0xa8, 0x7d, 0xff, + // Stakeable locked output type ID + 0x00, 0x00, 0x00, 0x16, + // Locktime + 0x00, 0x00, 0x00, 0x00, 0x05, 0x39, 0x7f, 0xb1, + // secp256k1fx transfer output type ID + 0x00, 0x00, 0x00, 0x07, + // amount + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + // secp256k1fx output locktime + 0x00, 0x00, 0x00, 0x00, 0x00, 0xbc, 0x61, 0x4e, + // threshold + 0x00, 0x00, 0x00, 0x00, + // number of addresses + 0x00, 0x00, 0x00, 0x00, + // Outputs[1] + // custom asset ID + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + // Stakeable locked output type ID + 0x00, 0x00, 0x00, 0x16, + // Locktime + 0x00, 0x00, 0x00, 0x00, 0x34, 0x3e, 0xfc, 0xea, + // secp256k1fx transfer output type ID + 0x00, 0x00, 0x00, 0x07, + // amount + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + // secp256k1fx output locktime + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // threshold + 0x00, 0x00, 0x00, 0x01, + // number of addresses + 0x00, 0x00, 0x00, 0x01, + // address[0] + 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, + 0x44, 0x55, 0x66, 0x77, + // number of inputs + 0x00, 0x00, 0x00, 0x03, + // inputs[0] + // TxID + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + // Tx output index + 0x00, 0x00, 0x00, 0x01, + // Mainnet AVAX assetID + 0x21, 0xe6, 0x73, 0x17, 0xcb, 0xc4, 0xbe, 0x2a, + 0xeb, 0x00, 0x67, 0x7a, 0xd6, 0x46, 0x27, 0x78, + 0xa8, 0xf5, 0x22, 0x74, 0xb9, 0xd6, 0x05, 0xdf, + 0x25, 0x91, 0xb2, 0x30, 0x27, 0xa8, 0x7d, 0xff, + // secp256k1fx transfer input type ID + 0x00, 0x00, 0x00, 0x05, + // input amount = 1 Avax + 0x00, 0x00, 0x00, 0x00, 0x3b, 0x9a, 0xca, 0x00, + // number of signatures needed in input + 0x00, 0x00, 0x00, 0x02, + // index of first signer + 0x00, 0x00, 0x00, 0x02, + // index of second signer + 0x00, 0x00, 0x00, 0x05, + // inputs[1] + // TxID + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + // Tx output index + 0x00, 0x00, 0x00, 0x02, + // Custom asset ID + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + // Stakeable locked input type ID + 0x00, 0x00, 0x00, 0x15, + // Locktime + 0x00, 0x00, 0x00, 0x00, 0x34, 0x3e, 0xfc, 0xea, + // secp256k1fx transfer input type ID + 0x00, 0x00, 0x00, 0x05, + // input amount + 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + // number of signatures needed in input + 0x00, 0x00, 0x00, 0x01, + // index of signer + 0x00, 0x00, 0x00, 0x00, + // inputs[2] + // TxID + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + // Tx output index + 0x00, 0x00, 0x00, 0x03, + // custom asset ID + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + 0x99, 0x77, 0x55, 0x77, 0x11, 0x33, 0x55, 0x31, + // secp256k1fx transfer input type ID + 0x00, 0x00, 0x00, 0x05, + // input amount + 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // number of signatures needed in input + 0x00, 0x00, 0x00, 0x00, + // length of memo + 0x00, 0x00, 0x00, 0x14, + // memo + 0xf0, 0x9f, 0x98, 0x85, 0x0a, 0x77, 0x65, 0x6c, + 0x6c, 0x20, 0x74, 0x68, 0x61, 0x74, 0x27, 0x73, + 0x01, 0x23, 0x45, 0x21, + } + var unsignedComplexBaseTx UnsignedTx = complexBaseTx + unsignedComplexBaseTxBytes, err := Codec.Marshal(Version, &unsignedComplexBaseTx) require.NoError(err) + require.Equal(expectedUnsignedComplexBaseTxBytes, unsignedComplexBaseTxBytes) + aliaser := ids.NewAliaser() + require.NoError(aliaser.Alias(constants.PlatformChainID, "P")) + + unsignedComplexBaseTx.InitCtx(&snow.Context{ + NetworkID: 1, + ChainID: constants.PlatformChainID, + AVAXAssetID: avaxAssetID, + BCLookup: aliaser, + }) + + unsignedComplexBaseTxJSONBytes, err := json.MarshalIndent(unsignedComplexBaseTx, "", "\t") + require.NoError(err) require.Equal(`{ - "networkID": 4, - "blockchainID": "SYXsAycDPUu4z2ZksJD5fh5nTDcH3vCFHnpcVye5XuJ2jArg", + "networkID": 1, + "blockchainID": "11111111111111111111111111111111LpoYY", "outputs": [ { - "assetID": "2KdbbWvpeAShCx5hGbtdF15FMMepq9kajsNTqVvvEbhiCRSxU", - "fxID": "2mB8TguRrYvbGw7G2UBqKfmL8osS7CfmzAAHSzuZK8bwpRKdY", + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", "output": { - "Err": null, - "Val": 100 + "locktime": 87654321, + "output": { + "addresses": [], + "amount": 1, + "locktime": 12345678, + "threshold": 0 + } + } + }, + { + "assetID": "2Ab62uWwJw1T6VvmKD36ufsiuGZuX1pGykXAvPX1LtjTRHxwcc", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "output": { + "locktime": 876543210, + "output": { + "addresses": [ + "P-avax1g32kvaugnx4tk3z4vemc3xd2hdz92enh972wxr" + ], + "amount": 18446744073709551615, + "locktime": 0, + "threshold": 1 + } } } ], "inputs": [ { - "txID": "t64jLxDRmxo8y48WjbRALPAZuSDZ6qPVaaeDzxHA4oSojhLt", - "outputIndex": 5, - "assetID": "2KdbbWvpeAShCx5hGbtdF15FMMepq9kajsNTqVvvEbhiCRSxU", - "fxID": "2mB8TguRrYvbGw7G2UBqKfmL8osS7CfmzAAHSzuZK8bwpRKdY", + "txID": "2wiU5PnFTjTmoAXGZutHAsPF36qGGyLHYHj9G1Aucfmb3JFFGN", + "outputIndex": 1, + "assetID": "FvwEAhmxKfeiG8SnEvq42hc6whRyY3EFYAvebMqDNDGCgxN5Z", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "amount": 1000000000, + "signatureIndices": [ + 2, + 5 + ] + } + }, + { + "txID": "2wiU5PnFTjTmoAXGZutHAsPF36qGGyLHYHj9G1Aucfmb3JFFGN", + "outputIndex": 2, + "assetID": "2Ab62uWwJw1T6VvmKD36ufsiuGZuX1pGykXAvPX1LtjTRHxwcc", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", + "input": { + "locktime": 876543210, + "input": { + "amount": 17293822569102704639, + "signatureIndices": [ + 0 + ] + } + } + }, + { + "txID": "2wiU5PnFTjTmoAXGZutHAsPF36qGGyLHYHj9G1Aucfmb3JFFGN", + "outputIndex": 3, + "assetID": "2Ab62uWwJw1T6VvmKD36ufsiuGZuX1pGykXAvPX1LtjTRHxwcc", + "fxID": "spdxUxVJQbX85MGxMHbKw1sHxMnSqJ3QBzDyDYEP3h6TLuxqQ", "input": { - "Err": null, - "Val": 100 + "amount": 1152921504606846976, + "signatureIndices": [] } } ], - "memo": "0x010203" -}`, string(txJSONBytes)) + "memo": "0xf09f98850a77656c6c2074686174277301234521" +}`, string(unsignedComplexBaseTxJSONBytes)) } diff --git a/vms/platformvm/txs/builder/builder.go b/vms/platformvm/txs/builder/builder.go index 3f13ec2ecad9..6c796d085abb 100644 --- a/vms/platformvm/txs/builder/builder.go +++ b/vms/platformvm/txs/builder/builder.go @@ -92,6 +92,17 @@ type DecisionTxBuilder interface { keys []*secp256k1.PrivateKey, changeAddr ids.ShortID, ) (*txs.Tx, error) + + // amount: amount the sender is sending + // owner: recipient of the funds + // keys: keys to sign the tx and pay the amount + // changeAddr: address to send change to, if there is any + NewBaseTx( + amount uint64, + owner secp256k1fx.OutputOwners, + keys []*secp256k1.PrivateKey, + changeAddr ids.ShortID, + ) (*txs.Tx, error) } type ProposalTxBuilder interface { @@ -661,3 +672,43 @@ func (b *builder) NewTransferSubnetOwnershipTx( } return tx, tx.SyntacticVerify(b.ctx) } + +func (b *builder) NewBaseTx( + amount uint64, + owner secp256k1fx.OutputOwners, + keys []*secp256k1.PrivateKey, + changeAddr ids.ShortID, +) (*txs.Tx, error) { + toBurn, err := math.Add64(amount, b.cfg.TxFee) + if err != nil { + return nil, fmt.Errorf("amount (%d) + tx fee(%d) overflows", amount, b.cfg.TxFee) + } + ins, outs, _, signers, err := b.Spend(b.state, keys, 0, toBurn, changeAddr) + if err != nil { + return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) + } + + outs = append(outs, &avax.TransferableOutput{ + Asset: avax.Asset{ID: b.ctx.AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: amount, + OutputOwners: owner, + }, + }) + + avax.SortTransferableOutputs(outs, txs.Codec) + + utx := &txs.BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: b.ctx.NetworkID, + BlockchainID: b.ctx.ChainID, + Ins: ins, + Outs: outs, + }, + } + tx, err := txs.NewSigned(utx, txs.Codec, signers) + if err != nil { + return nil, err + } + return tx, tx.SyntacticVerify(b.ctx) +} diff --git a/vms/platformvm/txs/builder/mock_builder.go b/vms/platformvm/txs/builder/mock_builder.go index 79291afb7cd7..19f74a7bed2f 100644 --- a/vms/platformvm/txs/builder/mock_builder.go +++ b/vms/platformvm/txs/builder/mock_builder.go @@ -14,6 +14,7 @@ import ( ids "github.com/ava-labs/avalanchego/ids" secp256k1 "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" txs "github.com/ava-labs/avalanchego/vms/platformvm/txs" + secp256k1fx "github.com/ava-labs/avalanchego/vms/secp256k1fx" gomock "go.uber.org/mock/gomock" ) @@ -100,6 +101,21 @@ func (mr *MockBuilderMockRecorder) NewAdvanceTimeTx(arg0 interface{}) *gomock.Ca return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewAdvanceTimeTx", reflect.TypeOf((*MockBuilder)(nil).NewAdvanceTimeTx), arg0) } +// NewBaseTx mocks base method. +func (m *MockBuilder) NewBaseTx(arg0 uint64, arg1 secp256k1fx.OutputOwners, arg2 []*secp256k1.PrivateKey, arg3 ids.ShortID) (*txs.Tx, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewBaseTx", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(*txs.Tx) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// NewBaseTx indicates an expected call of NewBaseTx. +func (mr *MockBuilderMockRecorder) NewBaseTx(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewBaseTx", reflect.TypeOf((*MockBuilder)(nil).NewBaseTx), arg0, arg1, arg2, arg3) +} + // NewCreateChainTx mocks base method. func (m *MockBuilder) NewCreateChainTx(arg0 ids.ID, arg1 []byte, arg2 ids.ID, arg3 []ids.ID, arg4 string, arg5 []*secp256k1.PrivateKey, arg6 ids.ShortID) (*txs.Tx, error) { m.ctrl.T.Helper() diff --git a/vms/platformvm/txs/codec.go b/vms/platformvm/txs/codec.go index 1d4eac1f700e..d743376d1acb 100644 --- a/vms/platformvm/txs/codec.go +++ b/vms/platformvm/txs/codec.go @@ -8,6 +8,7 @@ import ( "github.com/ava-labs/avalanchego/codec" "github.com/ava-labs/avalanchego/codec/linearcodec" + "github.com/ava-labs/avalanchego/utils" "github.com/ava-labs/avalanchego/utils/wrappers" "github.com/ava-labs/avalanchego/vms/platformvm/signer" "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" @@ -103,5 +104,8 @@ func RegisterUnsignedTxsTypes(targetCodec linearcodec.Codec) error { } func RegisterDUnsignedTxsTypes(targetCodec linearcodec.Codec) error { - return targetCodec.RegisterType(&TransferSubnetOwnershipTx{}) + return utils.Err( + targetCodec.RegisterType(&TransferSubnetOwnershipTx{}), + targetCodec.RegisterType(&BaseTx{}), + ) } diff --git a/vms/platformvm/txs/executor/atomic_tx_executor.go b/vms/platformvm/txs/executor/atomic_tx_executor.go index 09d374b3a395..3b7dc60ec173 100644 --- a/vms/platformvm/txs/executor/atomic_tx_executor.go +++ b/vms/platformvm/txs/executor/atomic_tx_executor.go @@ -76,6 +76,10 @@ func (*AtomicTxExecutor) AddPermissionlessDelegatorTx(*txs.AddPermissionlessDele return ErrWrongTxType } +func (*AtomicTxExecutor) BaseTx(*txs.BaseTx) error { + return ErrWrongTxType +} + func (e *AtomicTxExecutor) ImportTx(tx *txs.ImportTx) error { return e.atomicTx(tx) } diff --git a/vms/platformvm/txs/executor/proposal_tx_executor.go b/vms/platformvm/txs/executor/proposal_tx_executor.go index dd66815b9c8f..bd329b3f2576 100644 --- a/vms/platformvm/txs/executor/proposal_tx_executor.go +++ b/vms/platformvm/txs/executor/proposal_tx_executor.go @@ -101,6 +101,10 @@ func (*ProposalTxExecutor) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershi return ErrWrongTxType } +func (*ProposalTxExecutor) BaseTx(*txs.BaseTx) error { + return ErrWrongTxType +} + func (e *ProposalTxExecutor) AddValidatorTx(tx *txs.AddValidatorTx) error { // AddValidatorTx is a proposal transaction until the Banff fork // activation. Following the activation, AddValidatorTxs must be issued into diff --git a/vms/platformvm/txs/executor/standard_tx_executor.go b/vms/platformvm/txs/executor/standard_tx_executor.go index 2aa9e9c4a400..22bab59afd3b 100644 --- a/vms/platformvm/txs/executor/standard_tx_executor.go +++ b/vms/platformvm/txs/executor/standard_tx_executor.go @@ -514,3 +514,34 @@ func (e *StandardTxExecutor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwn return nil } + +func (e *StandardTxExecutor) BaseTx(tx *txs.BaseTx) error { + if !e.Backend.Config.IsDActivated(e.State.GetTimestamp()) { + return ErrDUpgradeNotActive + } + + // Verify the tx is well-formed + if err := e.Tx.SyntacticVerify(e.Ctx); err != nil { + return err + } + + // Verify the flowcheck + if err := e.FlowChecker.VerifySpend( + tx, + e.State, + tx.Ins, + tx.Outs, + e.Tx.Creds, + map[ids.ID]uint64{ + e.Ctx.AVAXAssetID: e.Config.TxFee, + }, + ); err != nil { + return err + } + + // Consume the UTXOS + avax.Consume(e.State, tx.Ins) + // Produce the UTXOS + avax.Produce(e.State, e.Tx.ID(), tx.Outs) + return nil +} diff --git a/vms/platformvm/txs/executor/tx_mempool_verifier.go b/vms/platformvm/txs/executor/tx_mempool_verifier.go index aa8d1dfaeb86..6704ccbd0489 100644 --- a/vms/platformvm/txs/executor/tx_mempool_verifier.go +++ b/vms/platformvm/txs/executor/tx_mempool_verifier.go @@ -78,6 +78,10 @@ func (v *MempoolTxVerifier) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwne return v.standardTx(tx) } +func (v *MempoolTxVerifier) BaseTx(tx *txs.BaseTx) error { + return v.standardTx(tx) +} + func (v *MempoolTxVerifier) standardTx(tx txs.UnsignedTx) error { baseState, err := v.standardBaseState() if err != nil { diff --git a/vms/platformvm/txs/mempool/issuer.go b/vms/platformvm/txs/mempool/issuer.go index e24afb5282da..b56c10190cf8 100644 --- a/vms/platformvm/txs/mempool/issuer.go +++ b/vms/platformvm/txs/mempool/issuer.go @@ -79,6 +79,11 @@ func (i *issuer) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) error return nil } +func (i *issuer) BaseTx(*txs.BaseTx) error { + i.m.addDecisionTx(i.tx) + return nil +} + func (i *issuer) AddPermissionlessValidatorTx(*txs.AddPermissionlessValidatorTx) error { i.m.addStakerTx(i.tx) return nil diff --git a/vms/platformvm/txs/mempool/remover.go b/vms/platformvm/txs/mempool/remover.go index e418cf46c342..b21071b16465 100644 --- a/vms/platformvm/txs/mempool/remover.go +++ b/vms/platformvm/txs/mempool/remover.go @@ -62,6 +62,11 @@ func (r *remover) TransferSubnetOwnershipTx(*txs.TransferSubnetOwnershipTx) erro return nil } +func (r *remover) BaseTx(*txs.BaseTx) error { + r.m.removeDecisionTxs([]*txs.Tx{r.tx}) + return nil +} + func (r *remover) AddPermissionlessValidatorTx(*txs.AddPermissionlessValidatorTx) error { r.m.removeStakerTx(r.tx) return nil diff --git a/vms/platformvm/txs/visitor.go b/vms/platformvm/txs/visitor.go index 5476d73c7e86..05a21c355801 100644 --- a/vms/platformvm/txs/visitor.go +++ b/vms/platformvm/txs/visitor.go @@ -19,4 +19,5 @@ type Visitor interface { AddPermissionlessValidatorTx(*AddPermissionlessValidatorTx) error AddPermissionlessDelegatorTx(*AddPermissionlessDelegatorTx) error TransferSubnetOwnershipTx(*TransferSubnetOwnershipTx) error + BaseTx(*BaseTx) error } diff --git a/vms/platformvm/vm_test.go b/vms/platformvm/vm_test.go index 7b0bbba58cdf..ea8d43891dd6 100644 --- a/vms/platformvm/vm_test.go +++ b/vms/platformvm/vm_test.go @@ -2188,3 +2188,78 @@ func TestTransferSubnetOwnershipTx(t *testing.T) { } require.Equal(expectedOwner, subnetOwner) } + +func TestBaseTx(t *testing.T) { + require := require.New(t) + vm, _, _ := defaultVM(t) + vm.ctx.Lock.Lock() + defer func() { + require.NoError(vm.Shutdown(context.Background())) + vm.ctx.Lock.Unlock() + }() + + sendAmt := uint64(100000) + changeAddr := ids.ShortEmpty + + baseTx, err := vm.txBuilder.NewBaseTx( + sendAmt, + secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{ + keys[1].Address(), + }, + }, + []*secp256k1.PrivateKey{keys[0]}, + changeAddr, + ) + require.NoError(err) + + totalInputAmt := uint64(0) + key0InputAmt := uint64(0) + for inputID := range baseTx.Unsigned.InputIDs() { + utxo, err := vm.state.GetUTXO(inputID) + require.NoError(err) + require.IsType(&secp256k1fx.TransferOutput{}, utxo.Out) + castOut := utxo.Out.(*secp256k1fx.TransferOutput) + if castOut.AddressesSet().Equals(set.Of(keys[0].Address())) { + key0InputAmt += castOut.Amt + } + totalInputAmt += castOut.Amt + } + require.Equal(totalInputAmt, key0InputAmt) + + totalOutputAmt := uint64(0) + key0OutputAmt := uint64(0) + key1OutputAmt := uint64(0) + changeAddrOutputAmt := uint64(0) + for _, output := range baseTx.Unsigned.Outputs() { + require.IsType(&secp256k1fx.TransferOutput{}, output.Out) + castOut := output.Out.(*secp256k1fx.TransferOutput) + if castOut.AddressesSet().Equals(set.Of(keys[0].Address())) { + key0OutputAmt += castOut.Amt + } + if castOut.AddressesSet().Equals(set.Of(keys[1].Address())) { + key1OutputAmt += castOut.Amt + } + if castOut.AddressesSet().Equals(set.Of(changeAddr)) { + changeAddrOutputAmt += castOut.Amt + } + totalOutputAmt += castOut.Amt + } + require.Equal(totalOutputAmt, key0OutputAmt+key1OutputAmt+changeAddrOutputAmt) + + require.Equal(vm.TxFee, totalInputAmt-totalOutputAmt) + require.Equal(sendAmt, key1OutputAmt) + + require.NoError(vm.Builder.AddUnverifiedTx(baseTx)) + baseTxBlock, err := vm.Builder.BuildBlock(context.Background()) + require.NoError(err) + + baseTxRawBlock := baseTxBlock.(*blockexecutor.Block).Block + require.IsType(&block.BanffStandardBlock{}, baseTxRawBlock) + require.Contains(baseTxRawBlock.Txs(), baseTx) + + require.NoError(baseTxBlock.Verify(context.Background())) + require.NoError(baseTxBlock.Accept(context.Background())) + require.NoError(vm.SetPreference(context.Background(), vm.manager.LastAccepted())) +} diff --git a/wallet/chain/p/backend_visitor.go b/wallet/chain/p/backend_visitor.go index da2fc591ecd5..57d602354428 100644 --- a/wallet/chain/p/backend_visitor.go +++ b/wallet/chain/p/backend_visitor.go @@ -58,6 +58,10 @@ func (b *backendVisitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnersh return b.baseTx(&tx.BaseTx) } +func (b *backendVisitor) BaseTx(tx *txs.BaseTx) error { + return b.baseTx(tx) +} + func (b *backendVisitor) ImportTx(tx *txs.ImportTx) error { err := b.b.removeUTXOs( b.ctx, diff --git a/wallet/chain/p/signer_visitor.go b/wallet/chain/p/signer_visitor.go index 6df1687400ac..9dd6018ea2e3 100644 --- a/wallet/chain/p/signer_visitor.go +++ b/wallet/chain/p/signer_visitor.go @@ -51,6 +51,14 @@ func (*signerVisitor) RewardValidatorTx(*txs.RewardValidatorTx) error { return errUnsupportedTxType } +func (s *signerVisitor) BaseTx(tx *txs.BaseTx) error { + txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) + if err != nil { + return err + } + return sign(s.tx, false, txSigners) +} + func (s *signerVisitor) AddValidatorTx(tx *txs.AddValidatorTx) error { txSigners, err := s.getSigners(constants.PlatformChainID, tx.Ins) if err != nil {