diff --git a/mapper/pchain/test_data.go b/mapper/pchain/test_data.go index b2dfcdda..f453883f 100644 --- a/mapper/pchain/test_data.go +++ b/mapper/pchain/test_data.go @@ -2,9 +2,11 @@ package pchain import ( "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/formatting/address" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/platformvm/block" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/secp256k1fx" @@ -150,6 +152,7 @@ func buildExport() (*txs.Tx, *txs.ExportTx, map[string]*types.AccountIdentifier) return signedTx, exportTx, inputTxAccounts } +// TODO: Remove Post-Durango func buildAddDelegator() (*txs.Tx, *txs.AddDelegatorTx, map[string]*types.AccountIdentifier) { avaxAssetID, _ := ids.FromString("U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK") txID, _ := ids.FromString("2JQGX1MBdszAaeV6eApCZm7CBpc917qWiyQ2cygFRJ6WteDkre") @@ -219,6 +222,7 @@ func buildAddDelegator() (*txs.Tx, *txs.AddDelegatorTx, map[string]*types.Accoun return signedTx, addDelegator, inputTxAccounts } +// TODO: Remove Post-Durango func buildValidatorTx() (*txs.Tx, *txs.AddValidatorTx, map[string]*types.AccountIdentifier) { avaxAssetID, _ := ids.FromString("U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK") @@ -293,3 +297,154 @@ func buildValidatorTx() (*txs.Tx, *txs.AddValidatorTx, map[string]*types.Account return signedTx, addValidator, inputTxAccounts } + +func buildAddPermissionlessDelegator() (*txs.Tx, *txs.AddPermissionlessDelegatorTx, map[string]*types.AccountIdentifier) { + avaxAssetID, _ := ids.FromString("U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK") + txID, _ := ids.FromString("2JQGX1MBdszAaeV6eApCZm7CBpc917qWiyQ2cygFRJ6WteDkre") + outAddr, _ := address.ParseToID("P-fuji1gdkq8g208e3j4epyjmx65jglsw7vauh86l47ac") + validatorID, _ := ids.NodeIDFromString("NodeID-BFa1padLXBj7VHa2JYvYGzcTBPQGjPhUy") + stakeAddr, _ := address.ParseToID("P-fuji1l022sue7g2kzvrcuxughl30xkss2cj0az3e5r2") + rewardAddr, _ := address.ParseToID("P-fuji1l022sue7g2kzvrcuxughl30xkss2cj0az3e5r2") + addPermissionlessDelegator := &txs.AddPermissionlessDelegatorTx{ + BaseTx: txs.BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: uint32(5), + BlockchainID: [32]byte{}, + Outs: []*avax.TransferableOutput{{ + Asset: avax.Asset{ID: avaxAssetID}, + FxID: [32]byte{}, + Out: &secp256k1fx.TransferOutput{ + Amt: 996649063, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 9, + Threshold: 1, + Addrs: []ids.ShortID{outAddr}, + }, + }, + }}, + Ins: []*avax.TransferableInput{{ + UTXOID: avax.UTXOID{TxID: txID, OutputIndex: 0, Symbol: false}, + Asset: avax.Asset{ID: avaxAssetID}, + FxID: [32]byte{}, + In: &secp256k1fx.TransferInput{ + Amt: 1996649063, + Input: secp256k1fx.Input{SigIndices: []uint32{}}, + }, + }}, + Memo: []byte{}, + }, + }, + Validator: txs.Validator{ + NodeID: validatorID, + Start: 1656058022, + End: 1657872569, + Wght: 1000000000, + }, + Subnet: constants.PrimaryNetworkID, + StakeOuts: []*avax.TransferableOutput{{ + Asset: avax.Asset{ID: avaxAssetID}, + FxID: [32]byte{}, + Out: &secp256k1fx.TransferOutput{ + Amt: 1000000000, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{stakeAddr}, + }, + }, + }}, + DelegationRewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{rewardAddr}, + }, + } + + signedTx, _ := txs.NewSigned(addPermissionlessDelegator, block.Codec, nil) + + inputTxAccounts := map[string]*types.AccountIdentifier{} + inputTxAccounts[addPermissionlessDelegator.Ins[0].String()] = &types.AccountIdentifier{Address: stakeAddr.String()} + + return signedTx, addPermissionlessDelegator, inputTxAccounts +} + +func buildAddPermissionlessValidator() (*txs.Tx, *txs.AddPermissionlessValidatorTx, map[string]*types.AccountIdentifier) { + avaxAssetID, _ := ids.FromString("U8iRqJoiJm8xZHAacmvYyZVwqQx6uDNtQeP3CQ6fcgQk3JqnK") + txID, _ := ids.FromString("88tfp1Pkw9vyKrRtVNiMrghFBrre6Q6CzqPW1t7StDNX9PJEo") + stakeAddr, _ := address.ParseToID("P-fuji1ljdzyey6vu3hgn3cwg4j5lpy0svd6arlxpj6je") + rewardAddr, _ := address.ParseToID("P-fuji1ljdzyey6vu3hgn3cwg4j5lpy0svd6arlxpj6je") + validatorID, _ := ids.NodeIDFromString("NodeID-CCecHmRK3ANe92VyvASxkNav26W4vAVpX") + addPermissionlessValidator := &txs.AddPermissionlessValidatorTx{ + BaseTx: txs.BaseTx{ + BaseTx: avax.BaseTx{ + NetworkID: uint32(5), + BlockchainID: [32]byte{}, + Outs: nil, + Ins: []*avax.TransferableInput{ // two inputs, the second locktimed + { + UTXOID: avax.UTXOID{TxID: txID, OutputIndex: 0}, + Asset: avax.Asset{ID: avaxAssetID}, + FxID: [32]byte{}, + In: &secp256k1fx.TransferInput{ + Amt: 2000000000, + Input: secp256k1fx.Input{SigIndices: []uint32{1}}, + }, + }, + { + UTXOID: avax.UTXOID{TxID: txID, OutputIndex: 1}, + Asset: avax.Asset{ID: avaxAssetID}, + FxID: [32]byte{}, + In: &stakeable.LockIn{ + Locktime: uint64(1666781236), // a unix time + TransferableIn: &secp256k1fx.TransferInput{ + Amt: 2000000000, + Input: secp256k1fx.Input{SigIndices: []uint32{1}}, + }, + }, + }, + }, + Memo: []byte{}, + }, + }, + Validator: txs.Validator{ + NodeID: validatorID, + Start: 1656084079, + End: 1687620079, + Wght: 2000000000, + }, + Subnet: constants.PrimaryNetworkID, + Signer: &signer.Empty{}, + StakeOuts: []*avax.TransferableOutput{{ + Asset: avax.Asset{ID: avaxAssetID}, + FxID: [32]byte{}, + Out: &secp256k1fx.TransferOutput{ + Amt: 2000000000, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{stakeAddr}, + }, + }, + }}, + ValidatorRewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{rewardAddr}, + }, + DelegatorRewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{rewardAddr}, + }, + DelegationShares: 20000, + } + + signedTx, _ := txs.NewSigned(addPermissionlessValidator, block.Codec, nil) + + inputTxAccounts := map[string]*types.AccountIdentifier{ + addPermissionlessValidator.Ins[0].String(): {Address: stakeAddr.String()}, + addPermissionlessValidator.Ins[1].String(): {Address: stakeAddr.String()}, + } + + return signedTx, addPermissionlessValidator, inputTxAccounts +} diff --git a/mapper/pchain/tx_builder.go b/mapper/pchain/tx_builder.go index 33b67891..92f8c067 100644 --- a/mapper/pchain/tx_builder.go +++ b/mapper/pchain/tx_builder.go @@ -43,10 +43,10 @@ func BuildTx( case OpAddPermissionlessDelegator: return buildAddPermissionlessDelegatorTx(matches, payloadMetadata, codec, avaxAssetID) case OpAddValidator: - // TODO: Remove Post-Durango activation + // TODO: Remove Post-Durango return buildAddValidatorTx(matches, payloadMetadata, codec, avaxAssetID) case OpAddDelegator: - // TODO: Remove Post-Durango activation + // TODO: Remove Post-Durango return buildAddDelegatorTx(matches, payloadMetadata, codec, avaxAssetID) default: return nil, nil, fmt.Errorf("invalid tx type: %s", opType) @@ -124,7 +124,7 @@ func buildExportTx( return tx, signers, tx.Sign(codec, nil) } -// TODO: Remove Post-Durango activation +// TODO: Remove Post-Durango // [buildAddValidatorTx] returns a duly initialized tx if it does not err func buildAddValidatorTx( matches []*parser.Match, @@ -188,7 +188,7 @@ func buildAddValidatorTx( return tx, signers, tx.Sign(codec, nil) } -// TODO: Remove Post-Durango activation +// TODO: Remove Post-Durango // [buildAddDelegatorTx] returns a duly initialized tx if it does not err func buildAddDelegatorTx( matches []*parser.Match, diff --git a/mapper/pchain/tx_dependency_test.go b/mapper/pchain/tx_dependency_test.go index 6f48ba85..a67c2034 100644 --- a/mapper/pchain/tx_dependency_test.go +++ b/mapper/pchain/tx_dependency_test.go @@ -5,10 +5,12 @@ import ( "time" "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" "github.com/ava-labs/avalanchego/utils/timer/mockable" "github.com/ava-labs/avalanchego/vms/components/avax" "github.com/ava-labs/avalanchego/vms/platformvm/reward" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/secp256k1fx" @@ -114,6 +116,7 @@ func TestTxDependencyIsCreateChain(t *testing.T) { require.Equal(res, res2) } +// TODO: Remove Post-Durango func TestTxDependencyIsAddValidator(t *testing.T) { require := require.New(t) @@ -216,3 +219,113 @@ func TestTxDependencyIsAddValidator(t *testing.T) { res2 := dep.GetUtxos() require.Equal(res, res2) } + +func TestTxDependencyIsAddPermissionlessValidator(t *testing.T) { + require := require.New(t) + + var ( + clk = mockable.Clock{} + avaxAssetID = ids.GenerateTestID() + validatorWeight = uint64(2022) + ) + + in := &avax.TransferableInput{ + UTXOID: avax.UTXOID{ + TxID: ids.ID{'t', 'x', 'I', 'D'}, + OutputIndex: 2, + }, + Asset: avax.Asset{ID: avaxAssetID}, + In: &secp256k1fx.TransferInput{ + Amt: uint64(5678), + Input: secp256k1fx.Input{SigIndices: []uint32{0}}, + }, + } + out := &avax.TransferableOutput{ + Asset: avax.Asset{ID: avaxAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: uint64(1234), + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }, + }, + } + stake := &avax.TransferableOutput{ + Asset: avax.Asset{ID: avaxAssetID}, + Out: &stakeable.LockOut{ + Locktime: uint64(clk.Time().Add(time.Second).Unix()), + TransferableOut: &secp256k1fx.TransferOutput{ + Amt: validatorWeight, + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[0].PublicKey().Address()}, + }, + }, + }, + } + utx := &txs.AddPermissionlessValidatorTx{ + BaseTx: txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: uint32(1492), + BlockchainID: ids.GenerateTestID(), + Ins: []*avax.TransferableInput{in}, + Outs: []*avax.TransferableOutput{out}, + }}, + Validator: txs.Validator{ + NodeID: ids.GenerateTestNodeID(), + Start: uint64(clk.Time().Unix()), + End: uint64(clk.Time().Add(time.Hour).Unix()), + Wght: validatorWeight, + }, + Subnet: constants.PrimaryNetworkID, + Signer: &signer.Empty{}, + StakeOuts: []*avax.TransferableOutput{stake}, + ValidatorRewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[1].PublicKey().Address()}, + }, + DelegatorRewardsOwner: &secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{preFundedKeys[1].PublicKey().Address()}, + }, + DelegationShares: reward.PercentDenominator, + } + tx, err := txs.NewSigned(utx, txs.Codec, nil) + require.NoError(err) + + dep := &SingleTxDependency{Tx: tx} + res := dep.GetUtxos() + require.Len(res, 2) + + expectedUTXOs := []*avax.UTXO{ + { + UTXOID: avax.UTXOID{ + TxID: tx.ID(), + OutputIndex: 0, + }, + Asset: out.Asset, + Out: out.Out, + }, + { + UTXOID: avax.UTXOID{ + TxID: tx.ID(), + OutputIndex: 1, + }, + Asset: stake.Asset, + Out: stake.Out, + }, + } + + utxo, found := res[expectedUTXOs[0].UTXOID] + require.True(found) + require.Equal(utxo, expectedUTXOs[0]) + + utxo, found = res[expectedUTXOs[1].UTXOID] + require.True(found) + require.Equal(utxo, expectedUTXOs[1]) + + // show idempotency + res2 := dep.GetUtxos() + require.Equal(res, res2) +} diff --git a/mapper/pchain/tx_parser.go b/mapper/pchain/tx_parser.go index 751dddd7..0d9e95d0 100644 --- a/mapper/pchain/tx_parser.go +++ b/mapper/pchain/tx_parser.go @@ -272,7 +272,7 @@ func (t *TxParser) parseAddValidatorTx(txID ids.ID, tx *txs.AddValidatorTx) (*tx } func (t *TxParser) parseAddPermissionlessValidatorTx(txID ids.ID, tx *txs.AddPermissionlessValidatorTx) (*txOps, error) { - ops, err := t.baseTxToCombinedOperations(txID, &tx.BaseTx, OpAddValidator) + ops, err := t.baseTxToCombinedOperations(txID, &tx.BaseTx, OpAddPermissionlessValidator) if err != nil { return nil, err } @@ -308,7 +308,7 @@ func (t *TxParser) parseAddDelegatorTx(txID ids.ID, tx *txs.AddDelegatorTx) (*tx } func (t *TxParser) parseAddPermissionlessDelegatorTx(txID ids.ID, tx *txs.AddPermissionlessDelegatorTx) (*txOps, error) { - ops, err := t.baseTxToCombinedOperations(txID, &tx.BaseTx, OpAddDelegator) + ops, err := t.baseTxToCombinedOperations(txID, &tx.BaseTx, OpAddPermissionlessDelegator) if err != nil { return nil, err } diff --git a/mapper/pchain/tx_parser_test.go b/mapper/pchain/tx_parser_test.go index aef19fff..79cd2d71 100644 --- a/mapper/pchain/tx_parser_test.go +++ b/mapper/pchain/tx_parser_test.go @@ -144,6 +144,7 @@ func TestMapOutOperation(t *testing.T) { require.Nil(rosettaOutOp[0].Metadata["sig_indices"]) } +// TODO: Remove Post-Durango func TestMapAddValidatorTx(t *testing.T) { require := require.New(t) @@ -177,6 +178,40 @@ func TestMapAddValidatorTx(t *testing.T) { require.Equal(1, cntMetaType) } +func TestMapAddPermissionlessValidatorTx(t *testing.T) { + require := require.New(t) + + signedTx, addPermissionlessValidatorTx, inputAccounts := buildAddPermissionlessValidator() + + require.Len(addPermissionlessValidatorTx.Ins, 2) + require.Empty(addPermissionlessValidatorTx.Outs) + + ctrl := gomock.NewController(t) + pchainClient := client.NewMockPChainClient(ctrl) + parserCfg := TxParserConfig{ + IsConstruction: true, + Hrp: avaconstants.FujiHRP, + ChainIDs: chainIDs, + AvaxAssetID: avaxAssetID, + PChainClient: pchainClient, + } + parser, err := NewTxParser(parserCfg, inputAccounts, nil) + require.NoError(err) + rosettaTransaction, err := parser.Parse(signedTx) + require.NoError(err) + + total := len(addPermissionlessValidatorTx.Ins) + len(addPermissionlessValidatorTx.Outs) + len(addPermissionlessValidatorTx.StakeOuts) + require.Len(rosettaTransaction.Operations, total) + + cntTxType, cntInputMeta, cntOutputMeta, cntMetaType := verifyRosettaTransaction(rosettaTransaction.Operations, OpAddPermissionlessValidator, OpTypeStakeOutput) + + require.Equal(3, cntTxType) + require.Equal(2, cntInputMeta) + require.Zero(cntOutputMeta) + require.Equal(1, cntMetaType) +} + +// TODO: Remove Post-Durango func TestMapAddDelegatorTx(t *testing.T) { require := require.New(t) @@ -229,6 +264,58 @@ func TestMapAddDelegatorTx(t *testing.T) { require.Equal(OpTypeStakeOutput, rosettaTransaction.Operations[2].Metadata["type"]) } +func TestMapAddPermissionlessDelegatorTx(t *testing.T) { + require := require.New(t) + + signedTx, addPermissionlessDelegatorTx, inputAccounts := buildAddPermissionlessDelegator() + + require.Len(addPermissionlessDelegatorTx.Ins, 1) + require.Len(addPermissionlessDelegatorTx.Outs, 1) + require.Len(addPermissionlessDelegatorTx.StakeOuts, 1) + + ctrl := gomock.NewController(t) + pchainClient := client.NewMockPChainClient(ctrl) + parserCfg := TxParserConfig{ + IsConstruction: true, + Hrp: avaconstants.FujiHRP, + ChainIDs: chainIDs, + AvaxAssetID: avaxAssetID, + PChainClient: pchainClient, + } + parser, err := NewTxParser(parserCfg, inputAccounts, nil) + require.NoError(err) + rosettaTransaction, err := parser.Parse(signedTx) + require.NoError(err) + + total := len(addPermissionlessDelegatorTx.Ins) + len(addPermissionlessDelegatorTx.Outs) + len(addPermissionlessDelegatorTx.StakeOuts) + require.Len(rosettaTransaction.Operations, total) + + cntTxType, cntInputMeta, cntOutputMeta, cntMetaType := verifyRosettaTransaction(rosettaTransaction.Operations, OpAddPermissionlessDelegator, OpTypeStakeOutput) + + require.Equal(3, cntTxType) + require.Equal(1, cntInputMeta) + require.Equal(1, cntOutputMeta) + require.Equal(1, cntMetaType) + + require.Equal(types.CoinSpent, rosettaTransaction.Operations[0].CoinChange.CoinAction) + require.Nil(rosettaTransaction.Operations[1].CoinChange) + require.Nil(rosettaTransaction.Operations[2].CoinChange) + + require.Equal(addPermissionlessDelegatorTx.Ins[0].UTXOID.String(), rosettaTransaction.Operations[0].CoinChange.CoinIdentifier.Identifier) + + require.Equal(int64(0), rosettaTransaction.Operations[0].OperationIdentifier.Index) + require.Equal(int64(1), rosettaTransaction.Operations[1].OperationIdentifier.Index) + require.Equal(int64(2), rosettaTransaction.Operations[2].OperationIdentifier.Index) + + require.Equal(OpAddPermissionlessDelegator, rosettaTransaction.Operations[0].Type) + require.Equal(OpAddPermissionlessDelegator, rosettaTransaction.Operations[1].Type) + require.Equal(OpAddPermissionlessDelegator, rosettaTransaction.Operations[2].Type) + + require.Equal(OpTypeInput, rosettaTransaction.Operations[0].Metadata["type"]) + require.Equal(OpTypeOutput, rosettaTransaction.Operations[1].Metadata["type"]) + require.Equal(OpTypeStakeOutput, rosettaTransaction.Operations[2].Metadata["type"]) +} + func TestMapImportTx(t *testing.T) { require := require.New(t) signedTx, importTx, inputAccounts := buildImport() diff --git a/mapper/pchain/types.go b/mapper/pchain/types.go index 8945d66d..770f02cc 100644 --- a/mapper/pchain/types.go +++ b/mapper/pchain/types.go @@ -87,7 +87,7 @@ type StakingOptions struct { BLSProofOfPossession string `json:"bls_proof_of_possession"` ValidationRewardsOwners []string `json:"reward_addresses"` DelegationRewardsOwners []string `json:"delegator_reward_addresses"` - Start uint64 `json:"start"` // TODO: Remove Post-Durango Activation + Start uint64 `json:"start"` // TODO: Remove Post-Durango End uint64 `json:"end"` Subnet string `json:"subnet"` Shares uint32 `json:"shares"` @@ -122,7 +122,7 @@ type StakingMetadata struct { BLSProofOfPossession string `json:"bls_proof_of_possession"` ValidationRewardsOwners []string `json:"reward_addresses"` DelegationRewardsOwners []string `json:"delegator_reward_addresses"` - Start uint64 `json:"start"` // TODO: Remove Post-Durango Activation + Start uint64 `json:"start"` // TODO: Remove Post-Durango End uint64 `json:"end"` Subnet string `json:"subnet"` Shares uint32 `json:"shares"`