From c1d54fb54128d2da1cf32ad9945dde3eed96c90d Mon Sep 17 00:00:00 2001 From: xiaohuo Date: Thu, 15 Apr 2021 09:44:33 +0800 Subject: [PATCH] feat: support staking construction --- rosetta/common/errors.go | 7 + rosetta/common/operations.go | 105 ++++ rosetta/common/operations_test.go | 149 +++++ rosetta/services/block.go | 2 +- rosetta/services/construction_check.go | 25 +- rosetta/services/construction_create.go | 193 +++++- rosetta/services/construction_create_test.go | 493 ++++++++++++++- rosetta/services/construction_parse.go | 24 +- rosetta/services/construction_parse_test.go | 33 +- rosetta/services/construction_submit.go | 44 +- rosetta/services/mempool.go | 2 +- rosetta/services/tx_construction.go | 263 +++++++- rosetta/services/tx_format.go | 9 +- rosetta/services/tx_format_test.go | 6 +- rosetta/services/tx_operation.go | 71 ++- rosetta/services/tx_operation_components.go | 217 ++++++- .../services/tx_operation_components_test.go | 587 +++++++++++++++++- rosetta/services/tx_operation_test.go | 15 +- rpc/pool.go | 2 +- rpc/transaction.go | 2 +- rpc/v2/types.go | 52 +- staking/types/transaction.go | 4 + test/helpers/transaction.go | 2 +- 23 files changed, 2201 insertions(+), 106 deletions(-) diff --git a/rosetta/common/errors.go b/rosetta/common/errors.go index c749bbf39..5c83ec632 100644 --- a/rosetta/common/errors.go +++ b/rosetta/common/errors.go @@ -79,6 +79,13 @@ var ( Message: "invalid transaction construction", Retriable: false, } + + // InvalidStakingConstructionError .. + InvalidStakingConstructionError = types.Error{ + Code: 10, + Message: "invalid staking transaction construction", + Retriable: false, + } ) // NewError create a new error with a given detail structure diff --git a/rosetta/common/operations.go b/rosetta/common/operations.go index 969b0e84d..c8e6014de 100644 --- a/rosetta/common/operations.go +++ b/rosetta/common/operations.go @@ -4,6 +4,8 @@ import ( "encoding/json" "fmt" + "github.com/harmony-one/harmony/internal/common" + "github.com/coinbase/rosetta-sdk-go/types" rpcV2 "github.com/harmony-one/harmony/rpc/v2" @@ -23,6 +25,21 @@ const ( // NativeCrossShardTransferOperation is an operation that only affects the native currency. NativeCrossShardTransferOperation = "NativeCrossShardTransfer" + // CreateValidatorOperation is an operation that only affects the native currency. + CreateValidatorOperation = "CreateValidator" + + // EditValidatorOperation is an operation that only affects the native currency. + EditValidatorOperation = "EditValidator" + + // DelegateOperation is an operation that only affects the native currency. + DelegateOperation = "Delegate" + + // UndelegateOperation is an operation that only affects the native currency. + UndelegateOperation = "Undelegate" + + // CollectRewardsOperation is an operation that only affects the native currency. + CollectRewardsOperation = "CollectRewards" + // GenesisFundsOperation is a side effect operation for genesis block only. // Note that no transaction can be constructed with this operation. GenesisFundsOperation = "Genesis" @@ -122,3 +139,91 @@ func (s *CrossShardTransactionOperationMetadata) UnmarshalFromInterface(data int *s = T return nil } + +func (s *CreateValidatorOperationMetadata) UnmarshalFromInterface(data interface{}) error { + var T CreateValidatorOperationMetadata + dat, err := json.Marshal(data) + if err != nil { + return err + } + if err := json.Unmarshal(dat, &T); err != nil { + return err + } + *s = T + return nil +} + +func (s *EditValidatorOperationMetadata) UnmarshalFromInterface(data interface{}) error { + var T EditValidatorOperationMetadata + dat, err := json.Marshal(data) + if err != nil { + return err + } + if err := json.Unmarshal(dat, &T); err != nil { + return err + } + *s = T + return nil +} + +func (s *DelegateOperationMetadata) UnmarshalFromInterface(data interface{}) error { + var T DelegateOperationMetadata + dat, err := json.Marshal(data) + if err != nil { + return err + } + if err := json.Unmarshal(dat, &T); err != nil { + return err + } + if T.Amount == nil || T.ValidatorAddress == "" || T.DelegatorAddress == "" { + return fmt.Errorf("expected validator address & delegator address & amount be present for DelegateOperationMetadata") + } + + if !common.IsBech32Address(T.ValidatorAddress) || !common.IsBech32Address(T.DelegatorAddress) { + return fmt.Errorf("expected validator address & delegator address to be bech32 format for DelegateOperationMetadata") + } + + *s = T + return nil +} + +func (s *UndelegateOperationMetadata) UnmarshalFromInterface(data interface{}) error { + var T UndelegateOperationMetadata + dat, err := json.Marshal(data) + if err != nil { + return err + } + if err := json.Unmarshal(dat, &T); err != nil { + return err + } + + if T.Amount == nil || T.ValidatorAddress == "" || T.DelegatorAddress == "" { + return fmt.Errorf("expected validator address & delegator address & amount be present for UndelegateOperationMetadata") + } + + if !common.IsBech32Address(T.ValidatorAddress) || !common.IsBech32Address(T.DelegatorAddress) { + return fmt.Errorf("expected validator address & delegator address to be bech32 format for UndelegateOperationMetadata") + } + + *s = T + return nil +} + +func (s *CollectRewardsMetadata) UnmarshalFromInterface(data interface{}) error { + var T CollectRewardsMetadata + dat, err := json.Marshal(data) + if err != nil { + return err + } + if err := json.Unmarshal(dat, &T); err != nil { + return err + } + if T.DelegatorAddress == "" { + return fmt.Errorf("expected delegator address be present for CollectRewardsMetadata") + } + if !common.IsBech32Address(T.DelegatorAddress) { + return fmt.Errorf("expected delegator address to be bech32 format for CollectRewardsMetadata") + } + *s = T + return nil +} diff --git a/rosetta/common/operations_test.go b/rosetta/common/operations_test.go index a2e9c08f1..41ebbb9ba 100644 --- a/rosetta/common/operations_test.go +++ b/rosetta/common/operations_test.go @@ -1,6 +1,7 @@ package common import ( + "math/big" "reflect" "sort" "testing" @@ -81,3 +82,151 @@ func TestStakingOperationTypes(t *testing.T) { t.Errorf("operation types are invalid") } } + +func TestCreateValidatorOperationMetadata_UnmarshalFromInterface(t *testing.T) { + data := map[string]interface{}{ + "validatorAddress": "one1pdv9lrdwl0rg5vglh4xtyrv3wjk3wsqket7zxy", + "commissionRate": 100000000000000000, + "maxCommissionRate": 900000000000000000, + "maxChangeRate": 50000000000000000, + "minSelfDelegation": 10, + "maxTotalDelegation": 3000, + "amount": 100, + "name": "Alice", + "website": "alice.harmony.one", + "identity": "alice", + "securityContact": "Bob", + "details": "Don't mess with me!!!", + } + s := CreateValidatorOperationMetadata{} + err := s.UnmarshalFromInterface(data) + if err != nil { + t.Fatal(err) + } + if s.ValidatorAddress != "one1pdv9lrdwl0rg5vglh4xtyrv3wjk3wsqket7zxy" { + t.Fatal("wrong validator address") + } + if s.CommissionRate.Cmp(new(big.Int).SetInt64(100000000000000000)) != 0 { + t.Fatal("wrong commission rate") + } + if s.MaxCommissionRate.Cmp(new(big.Int).SetInt64(900000000000000000)) != 0 { + t.Fatal("wrong max commission rate") + } + if s.MaxChangeRate.Cmp(new(big.Int).SetInt64(50000000000000000)) != 0 { + t.Fatal("wrong max change rate") + } + if s.MinSelfDelegation.Cmp(new(big.Int).SetInt64(10)) != 0 { + t.Fatal("wrong min self delegation") + } + if s.MaxTotalDelegation.Cmp(new(big.Int).SetInt64(3000)) != 0 { + t.Fatal("wrong max total delegation") + } + if s.Amount.Cmp(new(big.Int).SetInt64(100)) != 0 { + t.Fatal("wrong amount") + } + if s.Name != "Alice" { + t.Fatal("wrong name") + } + if s.Website != "alice.harmony.one" { + t.Fatal("wrong website") + } + if s.Identity != "alice" { + t.Fatal("wrong identity") + } + if s.SecurityContact != "Bob" { + t.Fatal("wrong security contact") + } + if s.Details != "Don't mess with me!!!" { + t.Fatal("wrong detail") + } +} + +func TestEditValidatorOperationMetadata_UnmarshalFromInterface(t *testing.T) { + data := map[string]interface{}{ + "validatorAddress": "one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9", + "commissionRate": 100000000000000000, + "minSelfDelegation": 10, + "maxTotalDelegation": 3000, + "name": "Alice", + "website": "alice.harmony.one", + "identity": "alice", + "securityContact": "Bob", + "details": "Don't mess with me!!!", + } + s := EditValidatorOperationMetadata{} + err := s.UnmarshalFromInterface(data) + if err != nil { + t.Fatal(err) + } + if s.ValidatorAddress != "one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9" { + t.Fatal("wrong validator address") + } + if s.CommissionRate.Cmp(new(big.Int).SetInt64(100000000000000000)) != 0 { + t.Fatal("wrong commission rate") + } + if s.MinSelfDelegation.Cmp(new(big.Int).SetInt64(10)) != 0 { + t.Fatal("wrong min self delegation") + } + if s.MaxTotalDelegation.Cmp(new(big.Int).SetInt64(3000)) != 0 { + t.Fatal("wrong max total delegation") + } + if s.Name != "Alice" { + t.Fatal("wrong name") + } + if s.Website != "alice.harmony.one" { + t.Fatal("wrong website") + } + if s.Identity != "alice" { + t.Fatal("wrong identity") + } + if s.SecurityContact != "Bob" { + t.Fatal("wrong security contact") + } + if s.Details != "Don't mess with me!!!" { + t.Fatal("wrong detail") + } +} + +func TestDelegateOperationMetadata_UnmarshalFromInterface(t *testing.T) { + data := map[string]interface{}{ + "validatorAddress": "one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9", + "delegatorAddress": "one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9", + "amount": 20000, + } + s := DelegateOperationMetadata{} + err := s.UnmarshalFromInterface(data) + if err != nil { + t.Fatal(err) + } + if s.ValidatorAddress != "one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9" { + t.Fatal("wrong validator address") + } + if s.DelegatorAddress != "one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9" { + t.Fatal("wrong delegator address") + } + if s.Amount.Cmp(new(big.Int).SetInt64(20000)) != 0 { + t.Fatal("wrong amount") + } +} + +func TestUndelegateOperationMetadata_UnmarshalFromInterface(t *testing.T) { + data := map[string]interface{}{ + "validatorAddress": "one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9", + "delegatorAddress": "one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9", + "amount": 20000, + } + s := UndelegateOperationMetadata{} + err := s.UnmarshalFromInterface(data) + if err != nil { + t.Fatal(err) + } + if s.ValidatorAddress != "one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9" { + t.Fatal("wrong validator address") + } + if s.DelegatorAddress != "one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9" { + t.Fatal("wrong delegator address") + } + if s.Amount.Cmp(new(big.Int).SetInt64(20000)) != 0 { + t.Fatal("wrong amount") + } +} diff --git a/rosetta/services/block.go b/rosetta/services/block.go index e0803e00f..dde2262f0 100644 --- a/rosetta/services/block.go +++ b/rosetta/services/block.go @@ -183,7 +183,7 @@ func (s *BlockAPI) BlockTransaction( return nil, rosettaError } } - transaction, rosettaError = FormatTransaction(txInfo.tx, txInfo.receipt, contractInfo) + transaction, rosettaError = FormatTransaction(txInfo.tx, txInfo.receipt, contractInfo, true) if rosettaError != nil { return nil, rosettaError } diff --git a/rosetta/services/construction_check.go b/rosetta/services/construction_check.go index b27ccb3a5..b12b55466 100644 --- a/rosetta/services/construction_check.go +++ b/rosetta/services/construction_check.go @@ -6,6 +6,8 @@ import ( "fmt" "math/big" + "github.com/harmony-one/harmony/core" + "github.com/coinbase/rosetta-sdk-go/types" ethCommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" @@ -75,6 +77,22 @@ func (s *ConstructAPI) ConstructionPreprocess( "message": "sender address is not found for given operations", }) } + if request.Operations[0].Type == common.CreateValidatorOperation && len(txMetadata.SlotPubKeys) == 0 { + return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": "invalid slot public keys", + }) + } + if request.Operations[0].Type == common.CreateValidatorOperation && len(txMetadata.SlotKeySigs) == 0 { + return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": "invalid slot key signatures", + }) + } + if request.Operations[0].Type == common.EditValidatorOperation && (txMetadata.SlotPubKeyToAdd == "" || + txMetadata.SlotPubKeyToRemove == "" || txMetadata.SlotKeyToAddSig == "") { + return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": "slot pub key to add/remove or sig to add error", + }) + } if txMetadata.ToShardID != nil && txMetadata.FromShardID != nil && components.Type != common.NativeCrossShardTransferOperation && *txMetadata.ToShardID != *txMetadata.FromShardID { return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ @@ -222,9 +240,10 @@ func (s *ConstructAPI) ConstructionMetadata( ) } } else { - return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ - "message": "staking operations are not supported", - }) + estGasUsed, err = core.IntrinsicGas(data, false, false, + false, options.OperationType == common.CreateValidatorOperation) + estGasUsed *= 2 + } if err != nil { return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ diff --git a/rosetta/services/construction_create.go b/rosetta/services/construction_create.go index 8c5d01c30..66135ce4c 100644 --- a/rosetta/services/construction_create.go +++ b/rosetta/services/construction_create.go @@ -6,6 +6,10 @@ import ( "encoding/json" "fmt" + "github.com/harmony-one/harmony/crypto/bls" + common2 "github.com/harmony-one/harmony/internal/common" + "github.com/harmony-one/harmony/numeric" + "github.com/coinbase/rosetta-sdk-go/types" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/rlp" @@ -32,7 +36,7 @@ type WrappedTransaction struct { // unpackWrappedTransactionFromString .. func unpackWrappedTransactionFromString( - str string, + str string, signed bool, ) (*WrappedTransaction, hmyTypes.PoolTransaction, *types.Error) { wrappedTransaction := &WrappedTransaction{} if err := json.Unmarshal([]byte(str), wrappedTransaction); err != nil { @@ -54,13 +58,196 @@ func unpackWrappedTransactionFromString( var tx hmyTypes.PoolTransaction stream := rlp.NewStream(bytes.NewBuffer(wrappedTransaction.RLPBytes), 0) if wrappedTransaction.IsStaking { + index := 0 + if signed { + index = 1 + } stakingTx := &stakingTypes.StakingTransaction{} if err := stakingTx.DecodeRLP(stream); err != nil { return nil, nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ "message": errors.WithMessage(err, "rlp encoding error for staking transaction").Error(), }) } - tx = stakingTx + intendedReceipt := &hmyTypes.Receipt{ + GasUsed: stakingTx.GasLimit(), + } + formattedTx, rosettaError := FormatTransaction( + stakingTx, intendedReceipt, &ContractInfo{ContractCode: wrappedTransaction.ContractCode}, signed, + ) + if rosettaError != nil { + return nil, nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": rosettaError, + }) + } + + var stakingTransaction *stakingTypes.StakingTransaction + switch stakingTx.StakingType() { + case stakingTypes.DirectiveCreateValidator: + var createValidatorMsg common.CreateValidatorOperationMetadata + // to solve deserialization error + slotPubKeys := formattedTx.Operations[index].Metadata["slotPubKeys"] + delete(formattedTx.Operations[index].Metadata, "slotPubKeys") + slotKeySigs := formattedTx.Operations[index].Metadata["slotKeySigs"] + delete(formattedTx.Operations[index].Metadata, "slotKeySigs") + + err := createValidatorMsg.UnmarshalFromInterface(formattedTx.Operations[index].Metadata) + if err != nil { + return nil, nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": err, + }) + } + validatorAddr, err := common2.Bech32ToAddress(createValidatorMsg.ValidatorAddress) + if err != nil { + return nil, nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": err, + }) + } + stakePayloadMaker := func() (stakingTypes.Directive, interface{}) { + return stakingTypes.DirectiveCreateValidator, stakingTypes.CreateValidator{ + Description: stakingTypes.Description{ + Name: createValidatorMsg.Name, + Identity: createValidatorMsg.Identity, + Website: createValidatorMsg.Website, + SecurityContact: createValidatorMsg.SecurityContact, + Details: createValidatorMsg.Details, + }, + CommissionRates: stakingTypes.CommissionRates{ + Rate: numeric.Dec{createValidatorMsg.CommissionRate}, + MaxRate: numeric.Dec{createValidatorMsg.MaxCommissionRate}, + MaxChangeRate: numeric.Dec{createValidatorMsg.MaxChangeRate}, + }, + MinSelfDelegation: createValidatorMsg.MinSelfDelegation, + MaxTotalDelegation: createValidatorMsg.MaxTotalDelegation, + ValidatorAddress: validatorAddr, + Amount: createValidatorMsg.Amount, + SlotPubKeys: slotPubKeys.([]bls.SerializedPublicKey), + SlotKeySigs: slotKeySigs.([]bls.SerializedSignature), + } + } + stakingTransaction, _ = stakingTypes.NewStakingTransaction(stakingTx.Nonce(), stakingTx.GasLimit(), stakingTx.GasPrice(), stakePayloadMaker) + case stakingTypes.DirectiveEditValidator: + var editValidatorMsg common.EditValidatorOperationMetadata + // to solve deserialization error + slotKeyToRemove := formattedTx.Operations[index].Metadata["slotPubKeyToRemove"] + delete(formattedTx.Operations[index].Metadata, "slotPubKeyToRemove") + slotKeyToAdd := formattedTx.Operations[index].Metadata["slotPubKeyToAdd"] + delete(formattedTx.Operations[index].Metadata, "slotPubKeyToAdd") + slotKeySigs := formattedTx.Operations[index].Metadata["slotKeyToAddSig"] + delete(formattedTx.Operations[index].Metadata, "slotKeyToAddSig") + err := editValidatorMsg.UnmarshalFromInterface(formattedTx.Operations[index].Metadata) + if err != nil { + return nil, nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": err, + }) + } + validatorAddr, err := common2.Bech32ToAddress(editValidatorMsg.ValidatorAddress) + if err != nil { + return nil, nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": err, + }) + } + stakePayloadMaker := func() (stakingTypes.Directive, interface{}) { + return stakingTypes.DirectiveEditValidator, stakingTypes.EditValidator{ + ValidatorAddress: validatorAddr, + Description: stakingTypes.Description{ + Name: editValidatorMsg.Name, + Identity: editValidatorMsg.Identity, + Website: editValidatorMsg.Website, + SecurityContact: editValidatorMsg.SecurityContact, + Details: editValidatorMsg.Details, + }, + CommissionRate: &numeric.Dec{editValidatorMsg.CommissionRate}, + MinSelfDelegation: editValidatorMsg.MinSelfDelegation, + MaxTotalDelegation: editValidatorMsg.MaxTotalDelegation, + SlotKeyToAdd: slotKeyToAdd.(*bls.SerializedPublicKey), + SlotKeyToRemove: slotKeyToRemove.(*bls.SerializedPublicKey), + SlotKeyToAddSig: slotKeySigs.(*bls.SerializedSignature), + } + } + stakingTransaction, _ = stakingTypes.NewStakingTransaction(stakingTx.Nonce(), stakingTx.GasLimit(), stakingTx.GasPrice(), stakePayloadMaker) + case stakingTypes.DirectiveDelegate: + var delegateMsg common.DelegateOperationMetadata + err := delegateMsg.UnmarshalFromInterface(formattedTx.Operations[index].Metadata) + if err != nil { + return nil, nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": err, + }) + } + validatorAddr, err := common2.Bech32ToAddress(delegateMsg.ValidatorAddress) + if err != nil { + return nil, nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": err, + }) + } + delegatorAddr, err := common2.Bech32ToAddress(delegateMsg.DelegatorAddress) + if err != nil { + return nil, nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": err, + }) + } + stakePayloadMaker := func() (stakingTypes.Directive, interface{}) { + return stakingTypes.DirectiveDelegate, stakingTypes.Delegate{ + ValidatorAddress: validatorAddr, + DelegatorAddress: delegatorAddr, + Amount: delegateMsg.Amount, + } + } + stakingTransaction, _ = stakingTypes.NewStakingTransaction(stakingTx.Nonce(), stakingTx.GasLimit(), stakingTx.GasPrice(), stakePayloadMaker) + case stakingTypes.DirectiveUndelegate: + var undelegateMsg common.UndelegateOperationMetadata + err := undelegateMsg.UnmarshalFromInterface(formattedTx.Operations[index].Metadata) + if err != nil { + return nil, nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": err, + }) + } + validatorAddr, err := common2.Bech32ToAddress(undelegateMsg.ValidatorAddress) + if err != nil { + return nil, nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": err, + }) + } + delegatorAddr, err := common2.Bech32ToAddress(undelegateMsg.DelegatorAddress) + if err != nil { + return nil, nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": err, + }) + } + stakePayloadMaker := func() (stakingTypes.Directive, interface{}) { + return stakingTypes.DirectiveUndelegate, stakingTypes.Undelegate{ + ValidatorAddress: validatorAddr, + DelegatorAddress: delegatorAddr, + Amount: undelegateMsg.Amount, + } + } + stakingTransaction, _ = stakingTypes.NewStakingTransaction(stakingTx.Nonce(), stakingTx.GasLimit(), stakingTx.GasPrice(), stakePayloadMaker) + case stakingTypes.DirectiveCollectRewards: + var collectRewardsMsg common.CollectRewardsMetadata + err := collectRewardsMsg.UnmarshalFromInterface(formattedTx.Operations[index].Metadata) + if err != nil { + return nil, nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": err, + }) + } + delegatorAddr, err := common2.Bech32ToAddress(collectRewardsMsg.DelegatorAddress) + if err != nil { + return nil, nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": err, + }) + } + stakePayloadMaker := func() (stakingTypes.Directive, interface{}) { + return stakingTypes.DirectiveCollectRewards, stakingTypes.CollectRewards{ + DelegatorAddress: delegatorAddr, + } + } + stakingTransaction, _ = stakingTypes.NewStakingTransaction(stakingTx.Nonce(), stakingTx.GasLimit(), stakingTx.GasPrice(), stakePayloadMaker) + default: + return nil, nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": "staking type error", + }) + } + stakingTransaction.SetRawSignature(stakingTx.RawSignatureValues()) + tx = stakingTransaction } else { plainTx := &hmyTypes.Transaction{} if err := plainTx.DecodeRLP(stream); err != nil { @@ -185,7 +372,7 @@ func (s *ConstructAPI) ConstructionCombine( if err := assertValidNetworkIdentifier(request.NetworkIdentifier, s.hmy.ShardID); err != nil { return nil, err } - wrappedTransaction, tx, rosettaError := unpackWrappedTransactionFromString(request.UnsignedTransaction) + wrappedTransaction, tx, rosettaError := unpackWrappedTransactionFromString(request.UnsignedTransaction, false) if rosettaError != nil { return nil, rosettaError } diff --git a/rosetta/services/construction_create_test.go b/rosetta/services/construction_create_test.go index 815755785..6d424223b 100644 --- a/rosetta/services/construction_create_test.go +++ b/rosetta/services/construction_create_test.go @@ -2,10 +2,19 @@ package services import ( "bytes" + "crypto/ecdsa" "encoding/json" + "fmt" "math/big" + "strings" "testing" + common2 "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/harmony-one/harmony/crypto/bls" + "github.com/harmony-one/harmony/internal/common" + "github.com/harmony-one/harmony/numeric" + "github.com/coinbase/rosetta-sdk-go/types" "github.com/ethereum/go-ethereum/crypto" @@ -47,7 +56,7 @@ func TestUnpackWrappedTransactionFromString(t *testing.T) { if err != nil { t.Fatal(err) } - testWrappedTx, testTx, rosettaError := unpackWrappedTransactionFromString(string(marshalledBytes)) + testWrappedTx, testTx, rosettaError := unpackWrappedTransactionFromString(string(marshalledBytes), true) if rosettaError != nil { t.Fatal(rosettaError) } @@ -83,7 +92,7 @@ func TestUnpackWrappedTransactionFromString(t *testing.T) { if err != nil { t.Fatal(err) } - testWrappedTx, testStx, rosettaError := unpackWrappedTransactionFromString(string(marshalledBytes)) + testWrappedTx, testStx, rosettaError := unpackWrappedTransactionFromString(string(marshalledBytes), true) if rosettaError != nil { t.Fatal(rosettaError) } @@ -97,7 +106,7 @@ func TestUnpackWrappedTransactionFromString(t *testing.T) { // Test invalid marshall marshalledBytesFail := marshalledBytes[:] marshalledBytesFail[0] = 0x0 - _, _, rosettaError = unpackWrappedTransactionFromString(string(marshalledBytesFail)) + _, _, rosettaError = unpackWrappedTransactionFromString(string(marshalledBytesFail), true) if rosettaError == nil { t.Fatal("expected error") } @@ -108,7 +117,7 @@ func TestUnpackWrappedTransactionFromString(t *testing.T) { if err != nil { t.Fatal(err) } - _, _, rosettaError = unpackWrappedTransactionFromString(string(marshalledBytesFail)) + _, _, rosettaError = unpackWrappedTransactionFromString(string(marshalledBytesFail), true) if rosettaError == nil { t.Fatal("expected error") } @@ -119,7 +128,7 @@ func TestUnpackWrappedTransactionFromString(t *testing.T) { if err != nil { t.Fatal(err) } - _, _, rosettaError = unpackWrappedTransactionFromString(string(marshalledBytesFail)) + _, _, rosettaError = unpackWrappedTransactionFromString(string(marshalledBytesFail), true) if rosettaError == nil { t.Fatal("expected error") } @@ -130,7 +139,7 @@ func TestUnpackWrappedTransactionFromString(t *testing.T) { if err != nil { t.Fatal(err) } - _, _, rosettaError = unpackWrappedTransactionFromString(string(marshalledBytesFail)) + _, _, rosettaError = unpackWrappedTransactionFromString(string(marshalledBytesFail), true) if rosettaError == nil { t.Fatal("expected error") } @@ -142,8 +151,478 @@ func TestUnpackWrappedTransactionFromString(t *testing.T) { if err != nil { t.Fatal(err) } - _, _, rosettaError = unpackWrappedTransactionFromString(string(marshalledBytesFail)) + _, _, rosettaError = unpackWrappedTransactionFromString(string(marshalledBytesFail), true) if rosettaError == nil { t.Fatal("expected error") } } + +func TestRecoverSenderAddressFromCreateValidatorString(t *testing.T) { + key, err := crypto.HexToECDSA("4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48") + if err != nil { + t.Fatal(err.Error()) + } + stakingTransaction, expectedPayload, err := stakingCreateValidatorTransaction(key) + if err != nil { + t.Fatal(err.Error()) + } + + address, err := stakingTransaction.SenderAddress() + if err != nil { + t.Fatal(err.Error()) + } + + if strings.ToLower(hexutil.Encode(address[:])) != "0xebcd16e8c1d8f493ba04e99a56474122d81a9c58" { + t.Fatal("address error") + } + + _, tx, rosettaError := unpackWrappedTransactionFromString("{\"rlp_bytes\":\"+QExgPkBIJQLWF+NrvvGijEfvUyyDZF0rRdAFvg4hUFsaWNlhWFsaWNlkWFsaWNlLmhhcm1vbnkub25lg0JvYpVEb24ndCBtZXNzIHdpdGggbWUhISHdyYgBY0V4XYoAAMmIDH1xO0naAADIh7GivC7FAACIiscjBInoAACJoqFdCVGb4AAA8bAwssOLExbakeBorDvYdRwJAe9sAqHVi8cSEEkYMCxu0D1YlGcdDIFtrStNMDMg8gL4YrhgaPgAtq32V7Z0kD4EcIBgkSuJO3x7UAeIgIJHVQqz4YblakTr88pIj47RpC9s7zoEvV0rK361p2eEjTE1s2LmaM5rukLHudVmbY46g75we1cI5yLFiTn+mwfBcPO3BiQUiQVrx14tYxAAAICEdzWUAIOhvkCAgIA=\",\"is_staking\":true,\"contract_code\":\"0x\",\"from\":{\"address\":\"one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9\",\"metadata\":{\"hex_address\":\"0xeBCD16e8c1D8f493bA04E99a56474122D81A9c58\"}}}", false) + if rosettaError != nil { + t.Fatal(rosettaError) + } + + signer := stakingTypes.NewEIP155Signer(new(big.Int).SetUint64(1)) + stakingTx, ok := tx.(*stakingTypes.StakingTransaction) + + if !ok { + t.Fatal() + } + sig, err := hexutil.Decode("0x5ee37d4f19016a7ba6eef5dad74f0214ecfb73feda46dd263c3dd0ed54f9d4f60d78033196f148b2a005fe0c1af9afbb0e6f087075f1c9addb3ec8d7d2b6b16600") + if err != nil { + t.Fatal(err) + } + stakingTx, err = stakingTx.WithSignature(signer, sig) + if err != nil { + t.Fatal(err) + } + v, r, s := stakingTransaction.RawSignatureValues() + v1, r1, s1 := stakingTx.RawSignatureValues() + if v.String() != v1.String() || r.String() != r1.String() || s.String() != s1.String() { + t.Log(stakingTransaction.RawSignatureValues()) + t.Log(stakingTx.RawSignatureValues()) + t.Fatal("signature error") + } + + if expectedPayload != signer.Hash(stakingTx) { + t.Error("payload error") + } + + address, err = stakingTx.SenderAddress() + if err != nil { + t.Fatal(err.Error()) + } + + if strings.ToLower(hexutil.Encode(address[:])) != "0xebcd16e8c1d8f493ba04e99a56474122d81a9c58" { + t.Fatal("address error") + } +} + +func TestRecoverSenderAddressFromEditValidatorString(t *testing.T) { + key, err := crypto.HexToECDSA("4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48") + if err != nil { + t.Fatal(err.Error()) + } + stakingTransaction, expectedPayload, err := stakingEditValidatorTransaction(key) + if err != nil { + t.Fatal(err.Error()) + } + + address, err := stakingTransaction.SenderAddress() + if err != nil { + t.Fatal(err.Error()) + } + + if strings.ToLower(hexutil.Encode(address[:])) != "0xebcd16e8c1d8f493ba04e99a56474122d81a9c58" { + t.Fatal("address error") + } + + // todo to correct rlp bytes + _, tx, rosettaError := unpackWrappedTransactionFromString("{\"rlp_bytes\":\"+QFBAfkBMZTrzRbowdj0k7oE6ZpWR0Ei2BqcWPg4hUFsaWNlhWFsaWNlkWFsaWNlLmhhcm1vbnkub25lg0JvYpVEb24ndCBtZXNzIHdpdGggbWUhISHJiAFjRXhdigAAiIrHIwSJ6AAAiaKhXQlRm+AAALAwssOLExbakeBorDvYdRwJAe9sAqHVi8cSEEkYMCxu0D1YlGcdDIFtrStNMDMg8gKwMLLDixMW2pHgaKw72HUcCQHvbAKh1YvHEhBJGDAsbtA9WJRnHQyBba0rTTAzIPICuGBo+AC2rfZXtnSQPgRwgGCRK4k7fHtQB4iAgkdVCrPhhuVqROvzykiPjtGkL2zvOgS9XSsrfrWnZ4SNMTWzYuZozmu6Qse51WZtjjqDvnB7VwjnIsWJOf6bB8Fw87cGJBSAgIR3NZQAgqQQgICA\",\"is_staking\":true,\"contract_code\":\"0x\",\"from\":{\"address\":\"one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9\",\"metadata\":{\"hex_address\":\"0xeBCD16e8c1D8f493bA04E99a56474122D81A9c58\"}}}", false) + if rosettaError != nil { + t.Fatal(rosettaError) + } + signer := stakingTypes.NewEIP155Signer(new(big.Int).SetUint64(1)) + stakingTx, ok := tx.(*stakingTypes.StakingTransaction) + if !ok { + t.Fatal() + } + sig, err := hexutil.Decode("0xe7d6edf1fc99806533bb2c5c5ece5873ddbf391c58ab2b839afb0770d86d70df6ccb4727afa97b0f26bec18dbb6bf287ef93867b2129af2d2a9bcced735d79f400") + if err != nil { + t.Fatal(err) + } + stakingTx, err = stakingTx.WithSignature(signer, sig) + if err != nil { + t.Fatal(err) + } + + v, r, s := stakingTransaction.RawSignatureValues() + v1, r1, s1 := stakingTx.RawSignatureValues() + if v.String() != v1.String() || r.String() != r1.String() || s.String() != s1.String() { + t.Log(stakingTransaction.RawSignatureValues()) + t.Log(stakingTx.RawSignatureValues()) + t.Fatal("signature error") + } + + if expectedPayload != signer.Hash(stakingTx) { + t.Error("payload error") + } + + address, err = stakingTx.SenderAddress() + if err != nil { + t.Fatal(err.Error()) + } + + if strings.ToLower(hexutil.Encode(address[:])) != "0xebcd16e8c1d8f493ba04e99a56474122d81a9c58" { + t.Fatal("address error") + } +} + +func TestRecoverSenderAddressFromDelegateString(t *testing.T) { + key, err := crypto.HexToECDSA("4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48") + if err != nil { + t.Fatal(err.Error()) + } + stakingTransaction, expectedPayload, err := stakingDelegateTransaction(key) + if err != nil { + t.Fatal(err.Error()) + } + + address, err := stakingTransaction.SenderAddress() + if err != nil { + t.Fatal(err.Error()) + } + + if strings.ToLower(hexutil.Encode(address[:])) != "0xebcd16e8c1d8f493ba04e99a56474122d81a9c58" { + t.Fatal("address error") + } + + _, tx, rosettaError := unpackWrappedTransactionFromString("{\"rlp_bytes\":\"+EEC85TrzRbowdj0k7oE6ZpWR0Ei2BqcWJTrzRbowdj0k7oE6ZpWR0Ei2BqcWIiKxyMEiegAAICEdzWUAIKkEICAgA==\",\"is_staking\":true,\"contract_code\":\"0x\",\"from\":{\"address\":\"one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9\",\"metadata\":{\"hex_address\":\"0xeBCD16e8c1D8f493bA04E99a56474122D81A9c58\"}}}", false) + if rosettaError != nil { + t.Fatal(rosettaError) + } + signer := stakingTypes.NewEIP155Signer(new(big.Int).SetUint64(1)) + stakingTx, ok := tx.(*stakingTypes.StakingTransaction) + if !ok { + t.Fatal() + } + sig, err := hexutil.Decode("0xfcfdda6ac52e81c5a4f53628588cd7fb2b0da40fb42f26472329698522230ca759fd3ba51b4078cc2b2370253688f1cc7f16706e897bf9fee0c1e7369f22dbf500") + if err != nil { + t.Fatal(err) + } + stakingTx, err = stakingTx.WithSignature(signer, sig) + if err != nil { + t.Fatal(err) + } + + v, r, s := stakingTransaction.RawSignatureValues() + v1, r1, s1 := stakingTx.RawSignatureValues() + if v.String() != v1.String() || r.String() != r1.String() || s.String() != s1.String() { + t.Log(stakingTransaction.RawSignatureValues()) + t.Log(stakingTx.RawSignatureValues()) + t.Fatal("signature error") + } + + if expectedPayload != signer.Hash(stakingTx) { + t.Error("payload error") + } + + address, err = stakingTx.SenderAddress() + if err != nil { + t.Fatal(err.Error()) + } + + if strings.ToLower(hexutil.Encode(address[:])) != "0xebcd16e8c1d8f493ba04e99a56474122d81a9c58" { + t.Fatal("address error") + } +} + +func TestRecoverSenderAddressFromUndelegateString(t *testing.T) { + key, err := crypto.HexToECDSA("4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48") + if err != nil { + t.Fatal(err.Error()) + } + stakingTransaction, expectedPayload, err := stakingUndelegateTransaction(key) + if err != nil { + t.Fatal(err.Error()) + } + + address, err := stakingTransaction.SenderAddress() + if err != nil { + t.Fatal(err.Error()) + } + + if strings.ToLower(hexutil.Encode(address[:])) != "0xebcd16e8c1d8f493ba04e99a56474122d81a9c58" { + t.Fatal("address error") + } + _, tx, rosettaError := unpackWrappedTransactionFromString("{\"rlp_bytes\":\"+EED85TrzRbowdj0k7oE6ZpWR0Ei2BqcWJTrzRbowdj0k7oE6ZpWR0Ei2BqcWIiKxyMEiegAAICEdzWUAIJSCICAgA==\",\"is_staking\":true,\"contract_code\":\"0x\",\"from\":{\"address\":\"one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9\",\"metadata\":{\"hex_address\":\"0xeBCD16e8c1D8f493bA04E99a56474122D81A9c58\"}}}", false) + if rosettaError != nil { + t.Fatal(rosettaError) + } + signer := stakingTypes.NewEIP155Signer(new(big.Int).SetUint64(1)) + stakingTx, ok := tx.(*stakingTypes.StakingTransaction) + if !ok { + t.Fatal() + } + sig, err := hexutil.Decode("0xb24e011d4013f0101a68ee0a6181e311fab6f65b768623f4ef8c6d5e429b507e3ce5dd52f3e85ae969854f7ac36a2ac74fe2d779ac9a9d8f38a64f683565980e00") + if err != nil { + t.Fatal(err) + } + stakingTx, err = stakingTx.WithSignature(signer, sig) + if err != nil { + t.Fatal(err) + } + + v, r, s := stakingTransaction.RawSignatureValues() + v1, r1, s1 := stakingTx.RawSignatureValues() + if v.String() != v1.String() || r.String() != r1.String() || s.String() != s1.String() { + t.Log(stakingTransaction.RawSignatureValues()) + t.Log(stakingTx.RawSignatureValues()) + t.Fatal("signature error") + } + + if expectedPayload != signer.Hash(stakingTx) { + t.Error("payload error") + } + + address, err = stakingTx.SenderAddress() + if err != nil { + t.Fatal(err.Error()) + } + + if strings.ToLower(hexutil.Encode(address[:])) != "0xebcd16e8c1d8f493ba04e99a56474122d81a9c58" { + t.Fatal("address error") + } +} + +func TestRecoverSenderAddressFromCollectRewardsString(t *testing.T) { + key, err := crypto.HexToECDSA("4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48") + if err != nil { + t.Fatal(err.Error()) + } + stakingTransaction, expectedPayload, err := stakingCollectRewardsTransaction(key) + if err != nil { + t.Fatal(err.Error()) + } + + address, err := stakingTransaction.SenderAddress() + if err != nil { + t.Fatal(err.Error()) + } + + if strings.ToLower(hexutil.Encode(address[:])) != "0xebcd16e8c1d8f493ba04e99a56474122d81a9c58" { + t.Fatal("address error") + } + + _, tx, rosettaError := unpackWrappedTransactionFromString("{\"rlp_bytes\":\"4wTVlOvNFujB2PSTugTpmlZHQSLYGpxYgIR3NZQAglIIgICA\",\"is_staking\":true,\"contract_code\":\"0x\",\"from\":{\"address\":\"one13lx3exmpfc446vsguc5d0mtgha2ff7h5uz85pk\",\"metadata\":{\"hex_address\":\"0x8fCD1C9B614E2b5D3208E628d7eD68bF5494faF4\"}}}", false) + if rosettaError != nil { + a, _ := json.Marshal(rosettaError) + fmt.Println(string(a)) + t.Fatal(rosettaError) + } + signer := stakingTypes.NewEIP155Signer(new(big.Int).SetUint64(1)) + stakingTx, ok := tx.(*stakingTypes.StakingTransaction) + if !ok { + t.Fatal() + } + sig, err := hexutil.Decode("0x13f25e40cf5cadf9ae68a318b216de52dc97ec423f581a36defccbdd31870cc26c66872f9f543246d0da7ee6b48d7e11e5034227795e80a5c1e95ac68a2a024500") + if err != nil { + t.Fatal(err) + } + stakingTx, err = stakingTx.WithSignature(signer, sig) + if err != nil { + t.Fatal(err) + } + + v, r, s := stakingTransaction.RawSignatureValues() + v1, r1, s1 := stakingTx.RawSignatureValues() + if v.String() != v1.String() || r.String() != r1.String() || s.String() != s1.String() { + t.Log(stakingTransaction.RawSignatureValues()) + t.Log(stakingTx.RawSignatureValues()) + t.Fatal("signature error") + } + + if expectedPayload != signer.Hash(stakingTx) { + t.Error("payload error") + } + + address, err = stakingTx.SenderAddress() + if err != nil { + t.Fatal(err.Error()) + } + + if strings.ToLower(hexutil.Encode(address[:])) != "0xebcd16e8c1d8f493ba04e99a56474122d81a9c58" { + t.Fatal("address error") + } +} + +func stakingCreateValidatorTransaction(key *ecdsa.PrivateKey) (*stakingTypes.StakingTransaction, common2.Hash, error) { + var pub bls.SerializedPublicKey + pubb, err := hexutil.Decode("0x30b2c38b1316da91e068ac3bd8751c0901ef6c02a1d58bc712104918302c6ed03d5894671d0c816dad2b4d303320f202") + if err != nil { + return nil, common2.Hash{}, err + } + copy(pub[:], pubb) + + var sig bls.SerializedSignature + sigg, err := hexutil.Decode("0x68f800b6adf657b674903e04708060912b893b7c7b500788808247550ab3e186e56a44ebf3ca488f8ed1a42f6cef3a04bd5d2b2b7eb5a767848d3135b362e668ce6bba42c7b9d5666d8e3a83be707b5708e722c58939fe9b07c170f3b7062414") + if err != nil { + return nil, common2.Hash{}, err + } + copy(sig[:], sigg) + validator, _ := common.Bech32ToAddress("one1pdv9lrdwl0rg5vglh4xtyrv3wjk3wsqket7zxy") + stakePayloadMaker := func() (stakingTypes.Directive, interface{}) { + return stakingTypes.DirectiveCreateValidator, stakingTypes.CreateValidator{ + Description: stakingTypes.Description{ + Name: "Alice", + Identity: "alice", + Website: "alice.harmony.one", + SecurityContact: "Bob", + Details: "Don't mess with me!!!", + }, + CommissionRates: stakingTypes.CommissionRates{ + Rate: numeric.Dec{new(big.Int).SetUint64(100000000000000000)}, + MaxRate: numeric.Dec{new(big.Int).SetUint64(900000000000000000)}, + MaxChangeRate: numeric.Dec{new(big.Int).SetUint64(50000000000000000)}, + }, + MinSelfDelegation: new(big.Int).Mul(new(big.Int).SetInt64(10), big.NewInt(1e18)), + MaxTotalDelegation: new(big.Int).Mul(new(big.Int).SetInt64(3000), big.NewInt(1e18)), + ValidatorAddress: validator, + SlotPubKeys: []bls.SerializedPublicKey{pub}, + SlotKeySigs: []bls.SerializedSignature{sig}, + Amount: new(big.Int).Mul(new(big.Int).SetInt64(100), big.NewInt(1e18)), + } + } + + gasPrice := big.NewInt(2000000000) + tx, _ := stakingTypes.NewStakingTransaction(0, 10600000, gasPrice, stakePayloadMaker) + + signer := stakingTypes.NewEIP155Signer(new(big.Int).SetUint64(1)) + signingPayload := signer.Hash(tx) + + stakingTransaction, err := stakingTypes.Sign(tx, signer, key) + if err != nil { + return nil, common2.Hash{}, err + } + + return stakingTransaction, signingPayload, nil +} + +func stakingEditValidatorTransaction(key *ecdsa.PrivateKey) (*stakingTypes.StakingTransaction, common2.Hash, error) { + stakePayloadMaker := func() (stakingTypes.Directive, interface{}) { + var slotKeyToRemove bls.SerializedPublicKey + removeBytes, _ := hexutil.Decode("0x30b2c38b1316da91e068ac3bd8751c0901ef6c02a1d58bc712104918302c6ed03d5894671d0c816dad2b4d303320f202") + copy(slotKeyToRemove[:], removeBytes) + + var slotKeyToAdd bls.SerializedPublicKey + addBytes, _ := hexutil.Decode("0x30b2c38b1316da91e068ac3bd8751c0901ef6c02a1d58bc712104918302c6ed03d5894671d0c816dad2b4d303320f202") + copy(slotKeyToAdd[:], addBytes) + + var slotKeyToAddSig bls.SerializedSignature + sigBytes, _ := hexutil.Decode("0x68f800b6adf657b674903e04708060912b893b7c7b500788808247550ab3e186e56a44ebf3ca488f8ed1a42f6cef3a04bd5d2b2b7eb5a767848d3135b362e668ce6bba42c7b9d5666d8e3a83be707b5708e722c58939fe9b07c170f3b7062414") + copy(slotKeyToAddSig[:], sigBytes) + + validator, _ := common.Bech32ToAddress("one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9") + + return stakingTypes.DirectiveEditValidator, stakingTypes.EditValidator{ + Description: stakingTypes.Description{ + Name: "Alice", + Identity: "alice", + Website: "alice.harmony.one", + SecurityContact: "Bob", + Details: "Don't mess with me!!!", + }, + CommissionRate: &numeric.Dec{new(big.Int).SetUint64(100000000000000000)}, + MinSelfDelegation: new(big.Int).Mul(new(big.Int).SetInt64(10), big.NewInt(1e18)), + MaxTotalDelegation: new(big.Int).Mul(new(big.Int).SetInt64(3000), big.NewInt(1e18)), + SlotKeyToRemove: &slotKeyToRemove, + SlotKeyToAdd: &slotKeyToAdd, + SlotKeyToAddSig: &slotKeyToAddSig, + ValidatorAddress: validator, + } + } + + gasPrice := big.NewInt(2000000000) + tx, _ := stakingTypes.NewStakingTransaction(0, 42000, gasPrice, stakePayloadMaker) + + signer := stakingTypes.NewEIP155Signer(new(big.Int).SetUint64(1)) + signingPayload := signer.Hash(tx) + + stakingTransaction, err := stakingTypes.Sign(tx, signer, key) + if err != nil { + return nil, common2.Hash{}, err + } + + return stakingTransaction, signingPayload, nil +} + +func stakingDelegateTransaction(key *ecdsa.PrivateKey) (*stakingTypes.StakingTransaction, common2.Hash, error) { + stakePayloadMaker := func() (stakingTypes.Directive, interface{}) { + + validator, _ := common.Bech32ToAddress("one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9") + delegator, _ := common.Bech32ToAddress("one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9") + return stakingTypes.DirectiveDelegate, stakingTypes.Delegate{ + ValidatorAddress: validator, + DelegatorAddress: delegator, + Amount: new(big.Int).Mul(new(big.Int).SetInt64(10), big.NewInt(1e18)), + } + } + + gasPrice := big.NewInt(2000000000) + tx, _ := stakingTypes.NewStakingTransaction(0, 42000, gasPrice, stakePayloadMaker) + + signer := stakingTypes.NewEIP155Signer(new(big.Int).SetUint64(1)) + signingPayload := signer.Hash(tx) + + stakingTransaction, err := stakingTypes.Sign(tx, signer, key) + if err != nil { + return nil, common2.Hash{}, err + } + return stakingTransaction, signingPayload, nil +} + +func stakingUndelegateTransaction(key *ecdsa.PrivateKey) (*stakingTypes.StakingTransaction, common2.Hash, error) { + stakePayloadMaker := func() (stakingTypes.Directive, interface{}) { + + validator, _ := common.Bech32ToAddress("one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9") + delegator, _ := common.Bech32ToAddress("one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9") + return stakingTypes.DirectiveUndelegate, stakingTypes.Undelegate{ + ValidatorAddress: validator, + DelegatorAddress: delegator, + Amount: new(big.Int).Mul(new(big.Int).SetInt64(10), big.NewInt(1e18)), + } + } + + gasPrice := big.NewInt(2000000000) + tx, _ := stakingTypes.NewStakingTransaction(0, 21000, gasPrice, stakePayloadMaker) + + signer := stakingTypes.NewEIP155Signer(new(big.Int).SetUint64(1)) + signingPayload := signer.Hash(tx) + + stakingTransaction, err := stakingTypes.Sign(tx, signer, key) + if err != nil { + return nil, common2.Hash{}, err + } + return stakingTransaction, signingPayload, nil +} + +func stakingCollectRewardsTransaction(key *ecdsa.PrivateKey) (*stakingTypes.StakingTransaction, common2.Hash, error) { + stakePayloadMaker := func() (stakingTypes.Directive, interface{}) { + + delegator, _ := common.Bech32ToAddress("one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9") + return stakingTypes.DirectiveCollectRewards, stakingTypes.CollectRewards{ + DelegatorAddress: delegator, + } + } + + gasPrice := big.NewInt(2000000000) + tx, _ := stakingTypes.NewStakingTransaction(0, 21000, gasPrice, stakePayloadMaker) + + signer := stakingTypes.NewEIP155Signer(new(big.Int).SetUint64(1)) + signingPayload := signer.Hash(tx) + + stakingTransaction, err := stakingTypes.Sign(tx, signer, key) + if err != nil { + return nil, common2.Hash{}, err + } + return stakingTransaction, signingPayload, nil +} diff --git a/rosetta/services/construction_parse.go b/rosetta/services/construction_parse.go index 9a91a1009..8f7527dd3 100644 --- a/rosetta/services/construction_parse.go +++ b/rosetta/services/construction_parse.go @@ -18,7 +18,7 @@ func (s *ConstructAPI) ConstructionParse( if err := assertValidNetworkIdentifier(request.NetworkIdentifier, s.hmy.ShardID); err != nil { return nil, err } - wrappedTransaction, tx, rosettaError := unpackWrappedTransactionFromString(request.Transaction) + wrappedTransaction, tx, rosettaError := unpackWrappedTransactionFromString(request.Transaction, request.Signed) if rosettaError != nil { return nil, rosettaError } @@ -30,7 +30,23 @@ func (s *ConstructAPI) ConstructionParse( if request.Signed { return parseSignedTransaction(ctx, wrappedTransaction, tx) } - return parseUnsignedTransaction(ctx, wrappedTransaction, tx) + + rsp, err := parseUnsignedTransaction(ctx, wrappedTransaction, tx) + if err != nil { + return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": err, + }) + } + + // it is unsigned as it reach to here, makes no sense, just to happy rosetta testing + switch rsp.Operations[0].Type { + case common.CreateValidatorOperation: + delete(rsp.Operations[0].Metadata, "slotPubKeys") + delete(rsp.Operations[0].Metadata, "slotKeySigs") + return rsp, nil + default: + return rsp, nil + } } // parseUnsignedTransaction .. @@ -54,7 +70,7 @@ func parseUnsignedTransaction( GasUsed: tx.GasLimit(), } formattedTx, rosettaError := FormatTransaction( - tx, intendedReceipt, &ContractInfo{ContractCode: wrappedTransaction.ContractCode}, + tx, intendedReceipt, &ContractInfo{ContractCode: wrappedTransaction.ContractCode}, false, ) if rosettaError != nil { return nil, rosettaError @@ -97,7 +113,7 @@ func parseSignedTransaction( GasUsed: tx.GasLimit(), } formattedTx, rosettaError := FormatTransaction( - tx, intendedReceipt, &ContractInfo{ContractCode: wrappedTransaction.ContractCode}, + tx, intendedReceipt, &ContractInfo{ContractCode: wrappedTransaction.ContractCode}, true, ) if rosettaError != nil { return nil, rosettaError diff --git a/rosetta/services/construction_parse_test.go b/rosetta/services/construction_parse_test.go index af9a0a0a2..c551bafc1 100644 --- a/rosetta/services/construction_parse_test.go +++ b/rosetta/services/construction_parse_test.go @@ -5,6 +5,8 @@ import ( "math/big" "testing" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/coinbase/rosetta-sdk-go/parser" "github.com/ethereum/go-ethereum/crypto" @@ -38,7 +40,7 @@ func TestParseUnsignedTransaction(t *testing.T) { refTestReceipt := &hmytypes.Receipt{ GasUsed: testTx.GasLimit(), } - refFormattedTx, rosettaError := FormatTransaction(testTx, refTestReceipt, &ContractInfo{}) + refFormattedTx, rosettaError := FormatTransaction(testTx, refTestReceipt, &ContractInfo{}, false) if rosettaError != nil { t.Fatal(rosettaError) } @@ -79,7 +81,32 @@ func TestParseUnsignedTransaction(t *testing.T) { } func TestParseUnsignedTransactionStaking(t *testing.T) { - // TODO (dm): implement staking test + bytes := "{\"rlp_bytes\":\"+DkC65TrzRbowdj0k7oE6ZpWR0Ei2BqcWJTrzRbowdj0k7oE6ZpWR0Ei2BqcWAqAhHc1lACCUgiAgIA=\",\"is_staking\":true,\"contract_code\":\"0x\",\"from\":{\"address\":\"one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9\",\"metadata\":{\"hex_address\":\"0xeBCD16e8c1D8f493bA04E99a56474122D81A9c58\"}}}" + wrappedTransaction, tx, rosettaError := unpackWrappedTransactionFromString(bytes, false) + if rosettaError != nil { + t.Fatal(rosettaError) + } + _, err := parseUnsignedTransaction(context.Background(), wrappedTransaction, tx) + if err != nil { + t.Fatal(err) + } + + bytes = "{\"rlp_bytes\":\"+HgC65TrzRbowdj0k7oE6ZpWR0Ei2BqcWJTrzRbowdj0k7oE6ZpWR0Ei2BqcWAoBhDuaygCCpBAon6JAUrdiS7lD8XyHhos2yj8gAff6d+If7EVYGijoRVOgKnIb+8ecrVX0wO/R1C0AX/FUL5AJ6jyOh1CtJ3kfV/c=\",\"is_staking\":true,\"contract_code\":\"0x\",\"from\":{\"address\":\"one1snm53h0yk5xrqcxrna0n3g3a4um7peqctwrt30\",\"metadata\":{\"hex_address\":\"0x84f748DDE4B50C3060c39f5f38a23dAF37E0e418\"}}}" + wrappedTransaction, tx, rosettaError = unpackWrappedTransactionFromString(bytes, true) + if rosettaError != nil { + t.Fatal(rosettaError) + } + addr, err2 := tx.SenderAddress() + if err2 != nil { + t.Fatal(err2) + } + if hexutil.Encode(addr[:]) != "0x84f748dde4b50c3060c39f5f38a23daf37e0e418" { + t.Fatal("sender addr error") + } + _, err = parseSignedTransaction(context.Background(), wrappedTransaction, tx) + if err != nil { + t.Fatal(err) + } } func TestParseSignedTransaction(t *testing.T) { @@ -101,7 +128,7 @@ func TestParseSignedTransaction(t *testing.T) { refTestReceipt := &hmytypes.Receipt{ GasUsed: testTx.GasLimit(), } - refFormattedTx, rosettaError := FormatTransaction(testTx, refTestReceipt, &ContractInfo{}) + refFormattedTx, rosettaError := FormatTransaction(testTx, refTestReceipt, &ContractInfo{}, true) if rosettaError != nil { t.Fatal(rosettaError) } diff --git a/rosetta/services/construction_submit.go b/rosetta/services/construction_submit.go index 3276ff5b5..df0bd62db 100644 --- a/rosetta/services/construction_submit.go +++ b/rosetta/services/construction_submit.go @@ -5,11 +5,10 @@ import ( "fmt" "github.com/coinbase/rosetta-sdk-go/types" - "github.com/pkg/errors" - hmyTypes "github.com/harmony-one/harmony/core/types" "github.com/harmony-one/harmony/rosetta/common" stakingTypes "github.com/harmony-one/harmony/staking/types" + "github.com/pkg/errors" ) // ConstructionHash implements the /construction/hash endpoint. @@ -19,7 +18,7 @@ func (s *ConstructAPI) ConstructionHash( if err := assertValidNetworkIdentifier(request.NetworkIdentifier, s.hmy.ShardID); err != nil { return nil, err } - _, tx, rosettaError := unpackWrappedTransactionFromString(request.SignedTransaction) + _, tx, rosettaError := unpackWrappedTransactionFromString(request.SignedTransaction, true) if rosettaError != nil { return nil, rosettaError } @@ -45,7 +44,7 @@ func (s *ConstructAPI) ConstructionSubmit( if err := assertValidNetworkIdentifier(request.NetworkIdentifier, s.hmy.ShardID); err != nil { return nil, err } - wrappedTransaction, tx, rosettaError := unpackWrappedTransactionFromString(request.SignedTransaction) + wrappedTransaction, tx, rosettaError := unpackWrappedTransactionFromString(request.SignedTransaction, true) if rosettaError != nil { return nil, rosettaError } @@ -55,45 +54,54 @@ func (s *ConstructAPI) ConstructionSubmit( }) } if tx.ShardID() != s.hmy.ShardID { - return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + return nil, common.NewError(common.StakingTransactionSubmissionError, map[string]interface{}{ "message": fmt.Sprintf("transaction is for shard %v != shard %v", tx.ShardID(), s.hmy.ShardID), }) } wrappedSenderAddress, err := getAddress(wrappedTransaction.From) if err != nil { - return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + return nil, common.NewError(common.StakingTransactionSubmissionError, map[string]interface{}{ "message": errors.WithMessage(err, "unable to get address from wrapped transaction"), }) } - txSenderAddress, err := tx.SenderAddress() + + var signedTx hmyTypes.PoolTransaction + if stakingTx, ok := tx.(*stakingTypes.StakingTransaction); ok && wrappedTransaction.IsStaking { + signedTx = stakingTx + } else if plainTx, ok := tx.(*hmyTypes.Transaction); ok && !wrappedTransaction.IsStaking { + signedTx = plainTx + } else { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": "invalid/inconsistent type or unknown transaction type stored in wrapped transaction", + }) + } + + txSenderAddress, err := signedTx.SenderAddress() if err != nil { - return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + return nil, common.NewError(common.StakingTransactionSubmissionError, map[string]interface{}{ "message": errors.WithMessage(err, "unable to get sender address from transaction").Error(), }) } + if wrappedSenderAddress != txSenderAddress { - return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + return nil, common.NewError(common.StakingTransactionSubmissionError, map[string]interface{}{ "message": "transaction sender address does not match wrapped transaction sender address", }) } - if stakingTx, ok := tx.(*stakingTypes.StakingTransaction); ok && wrappedTransaction.IsStaking { - if err := s.hmy.SendStakingTx(ctx, stakingTx); err != nil { + if wrappedTransaction.IsStaking { + if err := s.hmy.SendStakingTx(ctx, signedTx.(*stakingTypes.StakingTransaction)); err != nil { return nil, common.NewError(common.StakingTransactionSubmissionError, map[string]interface{}{ - "message": err.Error(), + "message": fmt.Sprintf("error is: %s, gas price is: %s, gas limit is: %d", err.Error(), signedTx.GasPrice().String(), signedTx.GasLimit()), }) } - } else if plainTx, ok := tx.(*hmyTypes.Transaction); ok && !wrappedTransaction.IsStaking { - if err := s.hmy.SendTx(ctx, plainTx); err != nil { + } else { + if err := s.hmy.SendTx(ctx, signedTx.(*hmyTypes.Transaction)); err != nil { return nil, common.NewError(common.TransactionSubmissionError, map[string]interface{}{ "message": err.Error(), }) } - } else { - return nil, common.NewError(common.CatchAllError, map[string]interface{}{ - "message": "invalid/inconsistent type or unknown transaction type stored in wrapped transaction", - }) } return &types.TransactionIdentifierResponse{ diff --git a/rosetta/services/mempool.go b/rosetta/services/mempool.go index efc63b54c..9c0711a51 100644 --- a/rosetta/services/mempool.go +++ b/rosetta/services/mempool.go @@ -84,7 +84,7 @@ func (s *MempoolAPI) MempoolTransaction( GasUsed: poolTx.GasLimit(), } - respTx, err := FormatTransaction(poolTx, estReceipt, &ContractInfo{}) + respTx, err := FormatTransaction(poolTx, estReceipt, &ContractInfo{}, true) if err != nil { return nil, err } diff --git a/rosetta/services/tx_construction.go b/rosetta/services/tx_construction.go index 14177c269..e3fbc29a0 100644 --- a/rosetta/services/tx_construction.go +++ b/rosetta/services/tx_construction.go @@ -3,9 +3,14 @@ package services import ( "encoding/json" "fmt" + "math/big" "github.com/coinbase/rosetta-sdk-go/types" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/harmony-one/harmony/crypto/bls" + common2 "github.com/harmony-one/harmony/internal/common" + "github.com/harmony-one/harmony/numeric" + types2 "github.com/harmony-one/harmony/staking/types" "github.com/pkg/errors" hmyTypes "github.com/harmony-one/harmony/core/types" @@ -22,6 +27,12 @@ type TransactionMetadata struct { ContractAccountIdentifier *types.AccountIdentifier `json:"contract_account_identifier,omitempty"` Data *string `json:"data,omitempty"` Logs []*hmyTypes.Log `json:"logs,omitempty"` + // SlotPubKeys SlotPubKeyToAdd SlotPubKeyToRemove are all hex representation of bls public key + SlotPubKeys []string `json:"slot_pub_keys,omitempty"` + SlotKeySigs []string `json:"slot_key_sigs,omitempty"` + SlotKeyToAddSig string `json:"slot_key_to_add_sig,omitempty"` + SlotPubKeyToAdd string `json:"slot_pub_key_to_add,omitempty"` + SlotPubKeyToRemove string `json:"slot_pub_key_to_remove,omitempty"` } // UnmarshalFromInterface .. @@ -39,7 +50,6 @@ func (t *TransactionMetadata) UnmarshalFromInterface(metaData interface{}) error } // ConstructTransaction object (unsigned). -// TODO (dm): implement staking transaction construction func ConstructTransaction( components *OperationComponents, metadata *ConstructMetadata, sourceShardID uint32, ) (response hmyTypes.PoolTransaction, rosettaError *types.Error) { @@ -69,6 +79,26 @@ func ConstructTransaction( if tx, rosettaError = constructPlainTransaction(components, metadata, sourceShardID); rosettaError != nil { return nil, rosettaError } + case common.CreateValidatorOperation: + if tx, rosettaError = constructCreateValidatorTransaction(components, metadata); rosettaError != nil { + return nil, rosettaError + } + case common.EditValidatorOperation: + if tx, rosettaError = constructEditValidatorTransaction(components, metadata); rosettaError != nil { + return nil, rosettaError + } + case common.DelegateOperation: + if tx, rosettaError = constructDelegateTransaction(components, metadata); rosettaError != nil { + return nil, rosettaError + } + case common.UndelegateOperation: + if tx, rosettaError = constructUndelegateTransaction(components, metadata); rosettaError != nil { + return nil, rosettaError + } + case common.CollectRewardsOperation: + if tx, rosettaError = constructCollectRewardsTransaction(components, metadata); rosettaError != nil { + return nil, rosettaError + } default: return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ "message": fmt.Sprintf("cannot create transaction with component type %v", components.Type), @@ -141,6 +171,237 @@ func constructContractCreationTransaction( ), nil } +func constructCreateValidatorTransaction( + components *OperationComponents, metadata *ConstructMetadata, +) (hmyTypes.PoolTransaction, *types.Error) { + createValidatorMsg := components.StakingMessage.(common.CreateValidatorOperationMetadata) + validatorAddr, err := common2.Bech32ToAddress(createValidatorMsg.ValidatorAddress) + if err != nil { + return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": errors.WithMessage(err, "convert validator address error").Error(), + }) + } + var slotPubKeys []bls.SerializedPublicKey + for _, slotPubKey := range metadata.Transaction.SlotPubKeys { + var pubKey bls.SerializedPublicKey + key, err := hexutil.Decode(slotPubKey) + if err != nil { + return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": errors.WithMessage(err, "decode slot public key error").Error(), + }) + } + copy(pubKey[:], key) + slotPubKeys = append(slotPubKeys, pubKey) + } + var slotKeySigs []bls.SerializedSignature + for _, slotKeySig := range metadata.Transaction.SlotKeySigs { + var keySig bls.SerializedSignature + sig, err := hexutil.Decode(slotKeySig) + if err != nil { + return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": errors.WithMessage(err, "decode slot key sig error").Error(), + }) + } + copy(keySig[:], sig) + slotKeySigs = append(slotKeySigs, keySig) + } + stakePayloadMaker := func() (types2.Directive, interface{}) { + return types2.DirectiveCreateValidator, types2.CreateValidator{ + Description: types2.Description{ + Name: createValidatorMsg.Name, + Identity: createValidatorMsg.Identity, + Website: createValidatorMsg.Website, + SecurityContact: createValidatorMsg.SecurityContact, + Details: createValidatorMsg.Details, + }, + CommissionRates: types2.CommissionRates{ + Rate: numeric.Dec{createValidatorMsg.CommissionRate}, + MaxRate: numeric.Dec{createValidatorMsg.MaxCommissionRate}, + MaxChangeRate: numeric.Dec{createValidatorMsg.MaxChangeRate}, + }, + MinSelfDelegation: new(big.Int).Mul(createValidatorMsg.MinSelfDelegation, big.NewInt(1e18)), + MaxTotalDelegation: new(big.Int).Mul(createValidatorMsg.MaxTotalDelegation, big.NewInt(1e18)), + ValidatorAddress: validatorAddr, + SlotPubKeys: slotPubKeys, + SlotKeySigs: slotKeySigs, + Amount: new(big.Int).Mul(createValidatorMsg.Amount, big.NewInt(1e18)), + } + } + + stakingTransaction, err := types2.NewStakingTransaction(metadata.Nonce, metadata.GasLimit, metadata.GasPrice, stakePayloadMaker) + if err != nil { + return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": errors.WithMessage(err, "new staking transaction error").Error(), + }) + } + + return stakingTransaction, nil +} + +func constructEditValidatorTransaction( + components *OperationComponents, metadata *ConstructMetadata, +) (hmyTypes.PoolTransaction, *types.Error) { + editValidatorMsg := components.StakingMessage.(common.EditValidatorOperationMetadata) + validatorAddr, err := common2.Bech32ToAddress(editValidatorMsg.ValidatorAddress) + if err != nil { + return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": errors.WithMessage(err, "convert validator address error").Error(), + }) + } + + var slotKeyToAdd bls.SerializedPublicKey + slotKeyToAddBytes, err := hexutil.Decode(metadata.Transaction.SlotPubKeyToAdd) + copy(slotKeyToAdd[:], slotKeyToAddBytes) + if err != nil { + return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": errors.WithMessage(err, "parse slotKeyToAdd error").Error(), + }) + } + + var slotKeyToRemove bls.SerializedPublicKey + slotKeyToRemoveBytes, err := hexutil.Decode(metadata.Transaction.SlotPubKeyToRemove) + copy(slotKeyToRemove[:], slotKeyToRemoveBytes) + if err != nil { + return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": errors.WithMessage(err, "parse slotKeyToRemove error").Error(), + }) + } + + var slotKeyToAddSig bls.SerializedSignature + SlotKeyToAddSigBytes, err := hexutil.Decode(metadata.Transaction.SlotKeyToAddSig) + copy(slotKeyToAddSig[:], SlotKeyToAddSigBytes) + if err != nil { + return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": errors.WithMessage(err, "parse slotKeyToAddSig error").Error(), + }) + } + + stakePayloadMaker := func() (types2.Directive, interface{}) { + return types2.DirectiveEditValidator, types2.EditValidator{ + ValidatorAddress: validatorAddr, + Description: types2.Description{ + Name: editValidatorMsg.Name, + Identity: editValidatorMsg.Identity, + Website: editValidatorMsg.Website, + SecurityContact: editValidatorMsg.SecurityContact, + Details: editValidatorMsg.Details, + }, + CommissionRate: &numeric.Dec{editValidatorMsg.CommissionRate}, + MinSelfDelegation: new(big.Int).Mul(editValidatorMsg.MinSelfDelegation, big.NewInt(1e18)), + MaxTotalDelegation: new(big.Int).Mul(editValidatorMsg.MaxTotalDelegation, big.NewInt(1e18)), + SlotKeyToAdd: &slotKeyToAdd, + SlotKeyToRemove: &slotKeyToRemove, + SlotKeyToAddSig: &slotKeyToAddSig, + } + } + + stakingTransaction, err := types2.NewStakingTransaction(metadata.Nonce, metadata.GasLimit, metadata.GasPrice, stakePayloadMaker) + if err != nil { + return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": errors.WithMessage(err, "new staking transaction error").Error(), + }) + } + + return stakingTransaction, nil +} + +func constructDelegateTransaction( + components *OperationComponents, metadata *ConstructMetadata, +) (hmyTypes.PoolTransaction, *types.Error) { + delegaterMsg := components.StakingMessage.(common.DelegateOperationMetadata) + delegatorAddr, err := common2.Bech32ToAddress(delegaterMsg.DelegatorAddress) + if err != nil { + return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": errors.WithMessage(err, "convert delegator address error").Error(), + }) + } + validatorAddr, err := common2.Bech32ToAddress(delegaterMsg.ValidatorAddress) + if err != nil { + return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": errors.WithMessage(err, "convert validator address error").Error(), + }) + } + + stakePayloadMaker := func() (types2.Directive, interface{}) { + return types2.DirectiveDelegate, types2.Delegate{ + DelegatorAddress: delegatorAddr, + ValidatorAddress: validatorAddr, + Amount: new(big.Int).Mul(delegaterMsg.Amount, big.NewInt(1e18)), + } + } + + stakingTransaction, err := types2.NewStakingTransaction(metadata.Nonce, metadata.GasLimit, metadata.GasPrice, stakePayloadMaker) + if err != nil { + return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": errors.WithMessage(err, "new staking transaction error").Error(), + }) + } + + return stakingTransaction, nil +} + +func constructUndelegateTransaction( + components *OperationComponents, metadata *ConstructMetadata, +) (hmyTypes.PoolTransaction, *types.Error) { + undelegaterMsg := components.StakingMessage.(common.UndelegateOperationMetadata) + delegatorAddr, err := common2.Bech32ToAddress(undelegaterMsg.DelegatorAddress) + if err != nil { + return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": errors.WithMessage(err, "convert delegator address error").Error(), + }) + } + validatorAddr, err := common2.Bech32ToAddress(undelegaterMsg.ValidatorAddress) + if err != nil { + return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": errors.WithMessage(err, "convert validator address error").Error(), + }) + } + + stakePayloadMaker := func() (types2.Directive, interface{}) { + return types2.DirectiveUndelegate, types2.Undelegate{ + DelegatorAddress: delegatorAddr, + ValidatorAddress: validatorAddr, + Amount: new(big.Int).Mul(undelegaterMsg.Amount, big.NewInt(1e18)), + } + } + + stakingTransaction, err := types2.NewStakingTransaction(metadata.Nonce, metadata.GasLimit, metadata.GasPrice, stakePayloadMaker) + if err != nil { + return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": errors.WithMessage(err, "new staking transaction error").Error(), + }) + } + + return stakingTransaction, nil +} + +func constructCollectRewardsTransaction( + components *OperationComponents, metadata *ConstructMetadata, +) (hmyTypes.PoolTransaction, *types.Error) { + collectRewardsMsg := components.StakingMessage.(common.CollectRewardsMetadata) + delegatorAddr, err := common2.Bech32ToAddress(collectRewardsMsg.DelegatorAddress) + if err != nil { + return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": errors.WithMessage(err, "convert delegator address error").Error(), + }) + } + + stakePayloadMaker := func() (types2.Directive, interface{}) { + return types2.DirectiveCollectRewards, types2.CollectRewards{ + DelegatorAddress: delegatorAddr, + } + } + + stakingTransaction, err := types2.NewStakingTransaction(metadata.Nonce, metadata.GasLimit, metadata.GasPrice, stakePayloadMaker) + if err != nil { + return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": errors.WithMessage(err, "new staking transaction error").Error(), + }) + } + + return stakingTransaction, nil +} + // constructPlainTransaction .. func constructPlainTransaction( components *OperationComponents, metadata *ConstructMetadata, sourceShardID uint32, diff --git a/rosetta/services/tx_format.go b/rosetta/services/tx_format.go index da68d60cd..3e17c2a6f 100644 --- a/rosetta/services/tx_format.go +++ b/rosetta/services/tx_format.go @@ -30,7 +30,7 @@ type ContractInfo struct { // FormatTransaction for staking, cross-shard sender, and plain transactions func FormatTransaction( - tx hmytypes.PoolTransaction, receipt *hmytypes.Receipt, contractInfo *ContractInfo, + tx hmytypes.PoolTransaction, receipt *hmytypes.Receipt, contractInfo *ContractInfo, signed bool, ) (fmtTx *types.Transaction, rosettaError *types.Error) { var operations []*types.Operation var isCrossShard, isStaking, isContractCreation bool @@ -40,7 +40,7 @@ func FormatTransaction( case *stakingTypes.StakingTransaction: isStaking = true stakingTx := tx.(*stakingTypes.StakingTransaction) - operations, rosettaError = GetNativeOperationsFromStakingTransaction(stakingTx, receipt) + operations, rosettaError = GetNativeOperationsFromStakingTransaction(stakingTx, receipt, signed) if rosettaError != nil { return nil, rosettaError } @@ -166,3 +166,8 @@ func negativeBigValue(num *big.Int) string { } return value } + +func positiveStringValue(amount string) string { + bigInt, _ := new(big.Int).SetString(amount, 10) + return new(big.Int).Abs(bigInt).String() +} diff --git a/rosetta/services/tx_format_test.go b/rosetta/services/tx_format_test.go index 6e88bb8b3..b4eeadbe9 100644 --- a/rosetta/services/tx_format_test.go +++ b/rosetta/services/tx_format_test.go @@ -78,7 +78,7 @@ func testFormatStakingTransaction( Status: hmytypes.ReceiptStatusSuccessful, GasUsed: gasUsed, } - rosettaTx, rosettaError := FormatTransaction(tx, receipt, &ContractInfo{}) + rosettaTx, rosettaError := FormatTransaction(tx, receipt, &ContractInfo{}, true) if rosettaError != nil { t.Fatal(rosettaError) } @@ -133,7 +133,7 @@ func testFormatPlainTransaction( Status: hmytypes.ReceiptStatusSuccessful, GasUsed: gasUsed, } - rosettaTx, rosettaError := FormatTransaction(tx, receipt, &ContractInfo{}) + rosettaTx, rosettaError := FormatTransaction(tx, receipt, &ContractInfo{}, true) if rosettaError != nil { t.Fatal(rosettaError) } @@ -190,7 +190,7 @@ func testFormatCrossShardSenderTransaction( Status: hmytypes.ReceiptStatusSuccessful, GasUsed: gasUsed, } - rosettaTx, rosettaError := FormatTransaction(tx, receipt, &ContractInfo{}) + rosettaTx, rosettaError := FormatTransaction(tx, receipt, &ContractInfo{}, true) if rosettaError != nil { t.Fatal(rosettaError) } diff --git a/rosetta/services/tx_operation.go b/rosetta/services/tx_operation.go index a7e414e37..5d938bfe3 100644 --- a/rosetta/services/tx_operation.go +++ b/rosetta/services/tx_operation.go @@ -74,7 +74,7 @@ func GetNativeOperationsFromTransaction( // GetNativeOperationsFromStakingTransaction for all staking directives // Note that only native token operations can come from staking transactions. func GetNativeOperationsFromStakingTransaction( - tx *stakingTypes.StakingTransaction, receipt *hmytypes.Receipt, + tx *stakingTypes.StakingTransaction, receipt *hmytypes.Receipt, signed bool, ) ([]*types.Operation, *types.Error) { senderAddress, err := tx.SenderAddress() if err != nil { @@ -85,12 +85,16 @@ func GetNativeOperationsFromStakingTransaction( return nil, rosettaError } - // All operations excepts for cross-shard tx payout expend gas - gasExpended := new(big.Int).Mul(new(big.Int).SetUint64(receipt.GasUsed), tx.GasPrice()) - gasOperations := newNativeOperationsWithGas(gasExpended, accountID) + var operations []*types.Operation + + if signed { + // All operations excepts for cross-shard tx payout expend gas + gasExpended := new(big.Int).Mul(new(big.Int).SetUint64(receipt.GasUsed), tx.GasPrice()) + operations = newNativeOperationsWithGas(gasExpended, accountID) + } // Format staking message for metadata using decimal numbers (hence usage of rpcV2) - rpcStakingTx, err := rpcV2.NewStakingTransaction(tx, ethcommon.Hash{}, 0, 0, 0) + rpcStakingTx, err := rpcV2.NewStakingTransaction(tx, ethcommon.Hash{}, 0, 0, 0, signed) if err != nil { return nil, common.NewError(common.CatchAllError, map[string]interface{}{ "message": err.Error(), @@ -125,26 +129,42 @@ func GetNativeOperationsFromStakingTransaction( } } - operations := append(gasOperations, &types.Operation{ - OperationIdentifier: &types.OperationIdentifier{ - Index: gasOperations[0].OperationIdentifier.Index + 1, - }, - Type: tx.StakingType().String(), - Status: GetTransactionStatus(tx, receipt), - Account: accountID, - Amount: amount, - Metadata: metadata, - }) - - // expose delegated balance - if tx.StakingType() == stakingTypes.DirectiveDelegate { - op2 := getDelegateOperationForSubAccount(tx, operations[1]) - return append(operations, op2), nil + if len(operations) > 0 { + operations = append(operations, &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: operations[0].OperationIdentifier.Index + 1, + }, + Type: tx.StakingType().String(), + Status: GetTransactionStatus(tx, receipt), + Account: accountID, + Amount: amount, + Metadata: metadata, + }) + } else { + operations = []*types.Operation{{ + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: tx.StakingType().String(), + Status: GetTransactionStatus(tx, receipt), + Account: accountID, + Amount: amount, + Metadata: metadata, + }} } - if tx.StakingType() == stakingTypes.DirectiveUndelegate { - op2 := getUndelegateOperationForSubAccount(tx, operations[1], receipt) - return append(operations, op2), nil + if signed { + + // expose delegated balance + if tx.StakingType() == stakingTypes.DirectiveDelegate { + op2 := getDelegateOperationForSubAccount(tx, operations[1]) + return append(operations, op2), nil + } + + if tx.StakingType() == stakingTypes.DirectiveUndelegate { + op2 := getUndelegateOperationForSubAccount(tx, operations[1], receipt) + return append(operations, op2), nil + } } return operations, nil @@ -635,11 +655,6 @@ func getAmountFromCollectRewards( break } } - if amount == nil { - return nil, common.NewError(common.CatchAllError, map[string]interface{}{ - "message": fmt.Sprintf("collect rewards amount not found for %v", senderAddress.String()), - }) - } return amount, nil } diff --git a/rosetta/services/tx_operation_components.go b/rosetta/services/tx_operation_components.go index 285af55a4..a8303f4d5 100644 --- a/rosetta/services/tx_operation_components.go +++ b/rosetta/services/tx_operation_components.go @@ -4,6 +4,8 @@ import ( "fmt" "math/big" + common2 "github.com/harmony-one/harmony/internal/common" + "github.com/coinbase/rosetta-sdk-go/types" "github.com/harmony-one/harmony/rosetta/common" @@ -38,7 +40,6 @@ func (s *OperationComponents) IsStaking() bool { // Providing a gas expenditure operation is INVALID. // All staking & cross-shard operations require metadata matching the operation type to be a valid. // All other operations do not require metadata. -// TODO (dm): implement staking transaction construction func GetOperationComponents( operations []*types.Operation, ) (*OperationComponents, *types.Error) { @@ -61,6 +62,16 @@ func GetOperationComponents( return getCrossShardOperationComponents(operations[0]) case common.ContractCreationOperation: return getContractCreationOperationComponents(operations[0]) + case common.CreateValidatorOperation: + return getCreateValidatorOperationComponents(operations[0]) + case common.EditValidatorOperation: + return getEditValidatorOperationComponents(operations[0]) + case common.DelegateOperation: + return getDelegateOperationComponents(operations[0]) + case common.UndelegateOperation: + return getUndelegateOperationComponents(operations[0]) + case common.CollectRewardsOperation: + return getCollectRewardsOperationComponents(operations[0]) default: return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ "message": fmt.Sprintf("%v is unsupported or invalid operation type", operations[0].Type), @@ -233,3 +244,207 @@ func getContractCreationOperationComponents( } return components, nil } + +func getCreateValidatorOperationComponents( + operation *types.Operation, +) (*OperationComponents, *types.Error) { + if operation == nil { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": "nil operation", + }) + } + metadata := common.CreateValidatorOperationMetadata{} + if err := metadata.UnmarshalFromInterface(operation.Metadata); err != nil { + return nil, common.NewError(common.InvalidStakingConstructionError, map[string]interface{}{ + "message": errors.WithMessage(err, "invalid metadata").Error(), + }) + } + if metadata.ValidatorAddress == "" || !common2.IsBech32Address(metadata.ValidatorAddress) { + return nil, common.NewError(common.InvalidStakingConstructionError, map[string]interface{}{ + "message": "validator address must not be empty or wrong format", + }) + } + if metadata.CommissionRate == nil || metadata.MaxCommissionRate == nil || metadata.MaxChangeRate == nil { + return nil, common.NewError(common.InvalidStakingConstructionError, map[string]interface{}{ + "message": "commission rate & max commission rate & max change rate must not be nil", + }) + } + if metadata.MinSelfDelegation == nil || metadata.MaxTotalDelegation == nil { + return nil, common.NewError(common.InvalidStakingConstructionError, map[string]interface{}{ + "message": "min self delegation & max total delegation much not be nil", + }) + } + if metadata.Amount == nil { + return nil, common.NewError(common.InvalidStakingConstructionError, map[string]interface{}{ + "message": "amount must not be nil", + }) + } + if metadata.Name == "" || metadata.Website == "" || metadata.Identity == "" || + metadata.SecurityContact == "" || metadata.Details == "" { + return nil, common.NewError(common.InvalidStakingConstructionError, map[string]interface{}{ + "message": "name & website & identity & security contract & details must no be empty", + }) + } + + // slot public key would be add into + // https://github.com/harmony-one/harmony/blob/3a8125666817149eaf9cea7870735e26cfe49c87/rosetta/services/tx_construction.go#L16 + // see https://github.com/harmony-one/harmony/issues/3431 + + components := &OperationComponents{ + Type: operation.Type, + From: operation.Account, + StakingMessage: metadata, + } + + if components.From == nil { + return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": "operation must have account sender/from identifier for creating validator", + }) + } + + return components, nil + +} + +func getEditValidatorOperationComponents( + operation *types.Operation, +) (*OperationComponents, *types.Error) { + if operation == nil { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": "nil operation", + }) + } + metadata := common.EditValidatorOperationMetadata{} + if err := metadata.UnmarshalFromInterface(operation.Metadata); err != nil { + return nil, common.NewError(common.InvalidStakingConstructionError, map[string]interface{}{ + "message": errors.WithMessage(err, "invalid metadata").Error(), + }) + } + if metadata.ValidatorAddress == "" || !common2.IsBech32Address(metadata.ValidatorAddress) { + return nil, common.NewError(common.InvalidStakingConstructionError, map[string]interface{}{ + "message": "validator address must not be empty or wrong format", + }) + } + if metadata.CommissionRate == nil || metadata.MinSelfDelegation == nil || metadata.MaxTotalDelegation == nil { + return nil, common.NewError(common.InvalidStakingConstructionError, map[string]interface{}{ + "message": "commission rate & max commission rate & max change rate must not be nil", + }) + } + if metadata.Name == "" || metadata.Website == "" || metadata.Identity == "" || + metadata.SecurityContact == "" || metadata.Details == "" { + return nil, common.NewError(common.InvalidStakingConstructionError, map[string]interface{}{ + "message": "name & website & identity & security contract & details must no be empty", + }) + } + + components := &OperationComponents{ + Type: operation.Type, + From: operation.Account, + StakingMessage: metadata, + } + + if components.From == nil { + return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": "operation must have account sender/from identifier for editing validator", + }) + } + + return components, nil + +} + +func getDelegateOperationComponents( + operation *types.Operation, +) (*OperationComponents, *types.Error) { + if operation == nil { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": "nil operation", + }) + } + metadata := common.DelegateOperationMetadata{} + if err := metadata.UnmarshalFromInterface(operation.Metadata); err != nil { + return nil, common.NewError(common.InvalidStakingConstructionError, map[string]interface{}{ + "message": errors.WithMessage(err, "invalid metadata").Error(), + }) + } + + // validator and delegator and amount already got checked inside UnmarshalFromInterface + components := &OperationComponents{ + Type: operation.Type, + From: operation.Account, + StakingMessage: metadata, + } + + if components.From == nil { + return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": "operation must have account sender/from identifier for delegating", + }) + } + + return components, nil + +} + +func getUndelegateOperationComponents( + operation *types.Operation, +) (*OperationComponents, *types.Error) { + if operation == nil { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": "nil operation", + }) + } + metadata := common.UndelegateOperationMetadata{} + if err := metadata.UnmarshalFromInterface(operation.Metadata); err != nil { + return nil, common.NewError(common.InvalidStakingConstructionError, map[string]interface{}{ + "message": errors.WithMessage(err, "invalid metadata").Error(), + }) + } + + // validator and delegator and amount already got checked inside UnmarshalFromInterface + components := &OperationComponents{ + Type: operation.Type, + From: operation.Account, + StakingMessage: metadata, + } + + if components.From == nil { + return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": "operation must have account sender/from identifier for undelegating", + }) + } + + return components, nil + +} + +func getCollectRewardsOperationComponents( + operation *types.Operation, +) (*OperationComponents, *types.Error) { + if operation == nil { + return nil, common.NewError(common.CatchAllError, map[string]interface{}{ + "message": "nil operation", + }) + } + metadata := common.CollectRewardsMetadata{} + if err := metadata.UnmarshalFromInterface(operation.Metadata); err != nil { + return nil, common.NewError(common.InvalidStakingConstructionError, map[string]interface{}{ + "message": errors.WithMessage(err, "invalid metadata").Error(), + }) + } + + //delegator already got checked inside UnmarshalFromInterface + + components := &OperationComponents{ + Type: operation.Type, + From: operation.Account, + StakingMessage: metadata, + } + + if components.From == nil { + return nil, common.NewError(common.InvalidTransactionConstructionError, map[string]interface{}{ + "message": "operation must have account sender/from identifier for collecting rewards", + }) + } + + return components, nil +} diff --git a/rosetta/services/tx_operation_components_test.go b/rosetta/services/tx_operation_components_test.go index 416c3ecc4..86dd76d11 100644 --- a/rosetta/services/tx_operation_components_test.go +++ b/rosetta/services/tx_operation_components_test.go @@ -6,7 +6,7 @@ import ( "github.com/coinbase/rosetta-sdk-go/types" "github.com/ethereum/go-ethereum/crypto" - + "github.com/harmony-one/harmony/crypto/bls" internalCommon "github.com/harmony-one/harmony/internal/common" "github.com/harmony-one/harmony/rosetta/common" ) @@ -490,6 +490,591 @@ func TestGetTransferOperationComponents(t *testing.T) { } } +func TestCreateValidatorOperationComponents(t *testing.T) { + refFromKey := internalCommon.MustGeneratePrivateKey() + refFrom, rosettaError := newAccountIdentifier(crypto.PubkeyToAddress(refFromKey.PublicKey)) + if rosettaError != nil { + t.Fatal(rosettaError) + } + validatorKey := internalCommon.MustGeneratePrivateKey() + validatorAddr := crypto.PubkeyToAddress(validatorKey.PublicKey) + validatorBech32Addr, _ := internalCommon.AddressToBech32(validatorAddr) + blsKey := bls.RandPrivateKey() + var serializedPubKey bls.SerializedPublicKey + copy(serializedPubKey[:], blsKey.GetPublicKey().Serialize()) + + // test valid operations + refOperations := &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: common.CreateValidatorOperation, + Account: refFrom, + Metadata: map[string]interface{}{ + "validatorAddress": validatorBech32Addr, + "commissionRate": new(big.Int).SetInt64(10), + "maxCommissionRate": new(big.Int).SetInt64(90), + "maxChangeRate": new(big.Int).SetInt64(2), + "minSelfDelegation": new(big.Int).SetInt64(10000), + "maxTotalDelegation": new(big.Int).SetInt64(10000000), + "amount": new(big.Int).SetInt64(100000), + "name": "Test validator", + "website": "https://test.website.com", + "identity": "test identity", + "securityContact": "security contact", + "details": "test detail", + }, + } + + testComponents, rosettaError := getCreateValidatorOperationComponents(refOperations) + if rosettaError != nil { + t.Fatal(rosettaError) + } + if testComponents.Type != refOperations.Type { + t.Error("expected same operation") + } + if testComponents.From == nil || types.Hash(testComponents.From) != types.Hash(refFrom) { + t.Error("expected same sender") + } + if testComponents.Amount != nil { + t.Error("expected nil amount") + } + if testComponents.To != nil { + t.Error("expected nil to") + } + + // test invalid operation + + // test nil account + refOperations = &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: common.CreateValidatorOperation, + Account: nil, + Metadata: map[string]interface{}{ + "validatorAddress": validatorAddr, + "commissionRate": new(big.Int).SetInt64(10), + "maxCommissionRate": new(big.Int).SetInt64(90), + "maxChangeRate": new(big.Int).SetInt64(2), + "minSelfDelegation": new(big.Int).SetInt64(10000), + "maxTotalDelegation": new(big.Int).SetInt64(10000000), + "amount": new(big.Int).SetInt64(100000), + "name": "Test validator", + "website": "https://test.website.com", + "identity": "test identity", + "securityContact": "security contact", + "details": "test detail", + }, + } + + _, rosettaError = getCreateValidatorOperationComponents(refOperations) + if rosettaError == nil { + t.Error("expected error") + } + + // test nil operation + _, rosettaError = getCreateValidatorOperationComponents(nil) + if rosettaError == nil { + t.Error("expected error") + } + + // test invalid validator + refOperations = &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: common.CreateValidatorOperation, + Account: refFrom, + Metadata: map[string]interface{}{ + "commissionRate": new(big.Int).SetInt64(10), + "maxCommissionRate": new(big.Int).SetInt64(90), + "maxChangeRate": new(big.Int).SetInt64(2), + "minSelfDelegation": new(big.Int).SetInt64(10000), + "maxTotalDelegation": new(big.Int).SetInt64(10000000), + "amount": new(big.Int).SetInt64(100000), + "name": "Test validator", + "website": "https://test.website.com", + "identity": "test identity", + "securityContact": "security contact", + "details": "test detail", + }, + } + _, rosettaError = getCreateValidatorOperationComponents(refOperations) + if rosettaError == nil { + t.Error("expected error") + } + + // test invalid commission rate + refOperations = &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: common.CreateValidatorOperation, + Account: refFrom, + Metadata: map[string]interface{}{ + "validatorAddress": validatorAddr, + "maxCommissionRate": new(big.Int).SetInt64(90), + "maxChangeRate": new(big.Int).SetInt64(2), + "minSelfDelegation": new(big.Int).SetInt64(10000), + "maxTotalDelegation": new(big.Int).SetInt64(10000000), + "amount": new(big.Int).SetInt64(100000), + "name": "Test validator", + "website": "https://test.website.com", + "identity": "test identity", + "securityContact": "security contact", + "details": "test detail", + }, + } + + _, rosettaError = getCreateValidatorOperationComponents(refOperations) + if rosettaError == nil { + t.Error("expected error") + } +} + +func TestEditValidatorOperationComponents(t *testing.T) { + refFromKey := internalCommon.MustGeneratePrivateKey() + refFrom, rosettaError := newAccountIdentifier(crypto.PubkeyToAddress(refFromKey.PublicKey)) + if rosettaError != nil { + t.Fatal(rosettaError) + } + validatorKey := internalCommon.MustGeneratePrivateKey() + validatorAddr := crypto.PubkeyToAddress(validatorKey.PublicKey) + validatorBech32Addr, _ := internalCommon.AddressToBech32(validatorAddr) + + // test valid operations + refOperations := &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: common.EditValidatorOperation, + Account: refFrom, + Metadata: map[string]interface{}{ + "validatorAddress": validatorBech32Addr, + "commissionRate": new(big.Int).SetInt64(10), + "minSelfDelegation": new(big.Int).SetInt64(10000), + "maxTotalDelegation": new(big.Int).SetInt64(10000000), + "name": "Test validator", + "website": "https://test.website.com", + "identity": "test identity", + "securityContact": "security contact", + "details": "test detail", + }, + } + + testComponents, rosettaError := getEditValidatorOperationComponents(refOperations) + if rosettaError != nil { + t.Fatal(rosettaError) + } + if testComponents.Type != refOperations.Type { + t.Error("expected same operation") + } + if testComponents.From == nil || types.Hash(testComponents.From) != types.Hash(refFrom) { + t.Error("expected same sender") + } + if testComponents.Amount != nil { + t.Error("expected nil amount") + } + if testComponents.To != nil { + t.Error("expected nil to") + } + + // test invalid operation + + // test nil account + refOperations = &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: common.EditValidatorOperation, + Metadata: map[string]interface{}{ + "validatorAddress": validatorAddr, + "commissionRate": new(big.Int).SetInt64(10), + "minSelfDelegation": new(big.Int).SetInt64(10000), + "maxTotalDelegation": new(big.Int).SetInt64(10000000), + "name": "Test validator", + "website": "https://test.website.com", + "identity": "test identity", + "securityContact": "security contact", + "details": "test detail", + }, + } + + _, rosettaError = getEditValidatorOperationComponents(refOperations) + if rosettaError == nil { + t.Error("expected error") + } + + // test nil operation + _, rosettaError = getEditValidatorOperationComponents(nil) + if rosettaError == nil { + t.Error("expected error") + } + + // test invalid validator + refOperations = &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: common.EditValidatorOperation, + Account: refFrom, + Metadata: map[string]interface{}{ + "commissionRate": new(big.Int).SetInt64(10), + "minSelfDelegation": new(big.Int).SetInt64(10000), + "maxTotalDelegation": new(big.Int).SetInt64(10000000), + "name": "Test validator", + "website": "https://test.website.com", + "identity": "test identity", + "securityContact": "security contact", + "details": "test detail", + }, + } + + _, rosettaError = getEditValidatorOperationComponents(refOperations) + if rosettaError == nil { + t.Error("expected error") + } + + // test invalid commission rate + refOperations = &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: common.EditValidatorOperation, + Account: refFrom, + Metadata: map[string]interface{}{ + "validatorAddress": validatorAddr, + "minSelfDelegation": new(big.Int).SetInt64(10000), + "maxTotalDelegation": new(big.Int).SetInt64(10000000), + "name": "Test validator", + "website": "https://test.website.com", + "identity": "test identity", + "securityContact": "security contact", + "details": "test detail", + }, + } + _, rosettaError = getEditValidatorOperationComponents(refOperations) + if rosettaError == nil { + t.Error("expected error") + } + + if rosettaError == nil { + t.Error("expected error") + } +} + +func TestDelegateOperationComponents(t *testing.T) { + refFromKey := internalCommon.MustGeneratePrivateKey() + delegatorAddr := crypto.PubkeyToAddress(refFromKey.PublicKey) + delegatorBech32Addr, _ := internalCommon.AddressToBech32(delegatorAddr) + refFrom, rosettaError := newAccountIdentifier(crypto.PubkeyToAddress(refFromKey.PublicKey)) + if rosettaError != nil { + t.Fatal(rosettaError) + } + validatorKey := internalCommon.MustGeneratePrivateKey() + validatorAddr, _ := internalCommon.AddressToBech32(crypto.PubkeyToAddress(validatorKey.PublicKey)) + + // test valid operations + refOperations := &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: common.DelegateOperation, + Account: refFrom, + Metadata: map[string]interface{}{ + "delegatorAddress": delegatorBech32Addr, + "validatorAddress": validatorAddr, + "amount": new(big.Int).SetInt64(100), + }, + } + + testComponents, rosettaError := getDelegateOperationComponents(refOperations) + if rosettaError != nil { + t.Fatal(rosettaError) + } + if testComponents.Type != refOperations.Type { + t.Error("expected same operation") + } + if testComponents.From == nil || types.Hash(testComponents.From) != types.Hash(refFrom) { + t.Error("expected same sender") + } + if testComponents.Amount != nil { + t.Error("expected nil amount") + } + if testComponents.To != nil { + t.Error("expected nil to") + } + + // test nil account + refOperations = &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: common.DelegateOperation, + Metadata: map[string]interface{}{ + "delegatorAddress": delegatorAddr, + "validatorAddress": validatorAddr, + "amount": new(big.Int).SetInt64(100), + }, + } + + _, rosettaError = getDelegateOperationComponents(refOperations) + if rosettaError == nil { + t.Error("expected error") + } + + // test nil operation + _, rosettaError = getDelegateOperationComponents(nil) + if rosettaError == nil { + t.Error("expected error") + } + + // test invalid delegator + refOperations = &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: common.DelegateOperation, + Metadata: map[string]interface{}{ + "validatorAddress": validatorAddr, + "amount": new(big.Int).SetInt64(100), + }, + } + + _, rosettaError = getDelegateOperationComponents(nil) + if rosettaError == nil { + t.Error("expected error") + } + + // test invalid validator + refOperations = &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: common.DelegateOperation, + Account: refFrom, + Metadata: map[string]interface{}{ + "delegatorAddress": delegatorAddr, + "amount": new(big.Int).SetInt64(100), + }, + } + + _, rosettaError = getDelegateOperationComponents(nil) + if rosettaError == nil { + t.Error("expected error") + } + + // test invalid amount + refOperations = &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: common.DelegateOperation, + Metadata: map[string]interface{}{ + "delegatorAddress": delegatorAddr, + "validatorAddress": validatorAddr, + }, + } + + _, rosettaError = getDelegateOperationComponents(nil) + if rosettaError == nil { + t.Error("expected error") + } + +} + +func TestUndelegateOperationComponents(t *testing.T) { + refFromKey := internalCommon.MustGeneratePrivateKey() + delegatorAddr := crypto.PubkeyToAddress(refFromKey.PublicKey) + delegatorBech32Addr, _ := internalCommon.AddressToBech32(delegatorAddr) + refFrom, rosettaError := newAccountIdentifier(crypto.PubkeyToAddress(refFromKey.PublicKey)) + if rosettaError != nil { + t.Fatal(rosettaError) + } + validatorKey := internalCommon.MustGeneratePrivateKey() + validatorAddr, _ := internalCommon.AddressToBech32(crypto.PubkeyToAddress(validatorKey.PublicKey)) + + // test valid operations + refOperations := &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: common.UndelegateOperation, + Account: refFrom, + Metadata: map[string]interface{}{ + "delegatorAddress": delegatorBech32Addr, + "validatorAddress": validatorAddr, + "amount": new(big.Int).SetInt64(100), + }, + } + + testComponents, rosettaError := getUndelegateOperationComponents(refOperations) + if rosettaError != nil { + t.Fatal(rosettaError) + } + if testComponents.Type != refOperations.Type { + t.Error("expected same operation") + } + if testComponents.From == nil || types.Hash(testComponents.From) != types.Hash(refFrom) { + t.Error("expected same sender") + } + if testComponents.Amount != nil { + t.Error("expected nil amount") + } + if testComponents.To != nil { + t.Error("expected nil to") + } + + // test nil account + refOperations = &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: common.UndelegateOperation, + Metadata: map[string]interface{}{ + "delegatorAddress": delegatorAddr, + "validatorAddress": validatorAddr, + "amount": new(big.Int).SetInt64(100), + }, + } + + _, rosettaError = getUndelegateOperationComponents(refOperations) + if rosettaError == nil { + t.Error("expected error") + } + + // test nil operation + _, rosettaError = getUndelegateOperationComponents(nil) + if rosettaError == nil { + t.Error("expected error") + } + + // test invalid delegator + refOperations = &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: common.UndelegateOperation, + Metadata: map[string]interface{}{ + "validatorAddress": validatorAddr, + "amount": new(big.Int).SetInt64(100), + }, + } + + _, rosettaError = getUndelegateOperationComponents(nil) + if rosettaError == nil { + t.Error("expected error") + } + + // test invalid validator + refOperations = &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: common.UndelegateOperation, + Account: refFrom, + Metadata: map[string]interface{}{ + "delegatorAddress": delegatorAddr, + "amount": new(big.Int).SetInt64(100), + }, + } + + _, rosettaError = getUndelegateOperationComponents(nil) + if rosettaError == nil { + t.Error("expected error") + } + + // test invalid amount + refOperations = &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: common.UndelegateOperation, + Metadata: map[string]interface{}{ + "delegatorAddress": delegatorAddr, + "validatorAddress": validatorAddr, + }, + } + + _, rosettaError = getUndelegateOperationComponents(nil) + if rosettaError == nil { + t.Error("expected error") + } + +} + +func TestCollectRewardsOperationComponents(t *testing.T) { + refFromKey := internalCommon.MustGeneratePrivateKey() + validatorAddr := crypto.PubkeyToAddress(refFromKey.PublicKey) + refFrom, rosettaError := newAccountIdentifier(validatorAddr) + delegatorBech32Addr, _ := internalCommon.AddressToBech32(validatorAddr) + if rosettaError != nil { + t.Fatal(rosettaError) + } + // test valid operations + refOperations := &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: common.EditValidatorOperation, + Account: refFrom, + Metadata: map[string]interface{}{ + "delegatorAddress": delegatorBech32Addr, + }, + } + + testComponents, rosettaError := getCollectRewardsOperationComponents(refOperations) + if rosettaError != nil { + t.Fatal(rosettaError) + } + if testComponents.Type != refOperations.Type { + t.Error("expected same operation") + } + if testComponents.From == nil || types.Hash(testComponents.From) != types.Hash(refFrom) { + t.Error("expected same sender") + } + if testComponents.Amount != nil { + t.Error("expected nil amount") + } + if testComponents.To != nil { + t.Error("expected nil to") + } + + // test invalid operation + + // test nil operation + refOperations = &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: common.EditValidatorOperation, + Metadata: map[string]interface{}{ + "delegatorAddress": validatorAddr, + }, + } + + _, rosettaError = getCollectRewardsOperationComponents(refOperations) + if rosettaError == nil { + t.Error("expected error") + } + + // test invalid delegator + refOperations = &types.Operation{ + OperationIdentifier: &types.OperationIdentifier{ + Index: 0, + }, + Type: common.EditValidatorOperation, + Account: refFrom, + Metadata: map[string]interface{}{}, + } + + _, rosettaError = getCollectRewardsOperationComponents(refOperations) + if rosettaError == nil { + t.Error("expected error") + } +} + func TestGetOperationComponents(t *testing.T) { refFromAmount := &types.Amount{ Value: "-12000", diff --git a/rosetta/services/tx_operation_test.go b/rosetta/services/tx_operation_test.go index 5dc5b7d7b..e75d8c818 100644 --- a/rosetta/services/tx_operation_test.go +++ b/rosetta/services/tx_operation_test.go @@ -1,6 +1,7 @@ package services import ( + "encoding/json" "fmt" "math/big" "reflect" @@ -74,12 +75,14 @@ func TestGetStakingOperationsFromCreateValidator(t *testing.T) { }, Metadata: metadata, }) - operations, rosettaError := GetNativeOperationsFromStakingTransaction(tx, receipt) + operations, rosettaError := GetNativeOperationsFromStakingTransaction(tx, receipt, true) if rosettaError != nil { t.Fatal(rosettaError) } if !reflect.DeepEqual(operations, refOperations) { - t.Errorf("Expected operations to be %v not %v", refOperations, operations) + operationsRaw, _ := json.Marshal(operations) + refOperationsRaw, _ := json.Marshal(refOperations) + t.Errorf("Expected operations to be:\n %v\n not\n %v", string(operationsRaw), string(refOperationsRaw)) } if err := assertNativeOperationTypeUniquenessInvariant(operations); err != nil { t.Error(err) @@ -231,7 +234,7 @@ func TestGetStakingOperationsFromDelegate(t *testing.T) { }, Metadata: metadata, }) - operations, rosettaError := GetNativeOperationsFromStakingTransaction(tx, receipt) + operations, rosettaError := GetNativeOperationsFromStakingTransaction(tx, receipt, true) if rosettaError != nil { t.Fatal(rosettaError) } @@ -365,7 +368,7 @@ func TestGetStakingOperationsFromUndelegate(t *testing.T) { }, Metadata: metadata, }) - operations, rosettaError := GetNativeOperationsFromStakingTransaction(tx, receipt) + operations, rosettaError := GetNativeOperationsFromStakingTransaction(tx, receipt, true) if rosettaError != nil { t.Fatal(rosettaError) } @@ -426,7 +429,7 @@ func TestGetStakingOperationsFromCollectRewards(t *testing.T) { }, Metadata: metadata, }) - operations, rosettaError := GetNativeOperationsFromStakingTransaction(tx, receipt) + operations, rosettaError := GetNativeOperationsFromStakingTransaction(tx, receipt, true) if rosettaError != nil { t.Fatal(rosettaError) } @@ -480,7 +483,7 @@ func TestGetStakingOperationsFromEditValidator(t *testing.T) { }, Metadata: metadata, }) - operations, rosettaError := GetNativeOperationsFromStakingTransaction(tx, receipt) + operations, rosettaError := GetNativeOperationsFromStakingTransaction(tx, receipt, true) if rosettaError != nil { t.Fatal(rosettaError) } diff --git a/rpc/pool.go b/rpc/pool.go index da0b98509..d1673dda5 100644 --- a/rpc/pool.go +++ b/rpc/pool.go @@ -262,7 +262,7 @@ func (s *PublicPoolService) PendingStakingTransactions( continue // Legacy behavior is to not return error here } case V2: - tx, err = v2.NewStakingTransaction(stakingTx, common.Hash{}, 0, 0, 0) + tx, err = v2.NewStakingTransaction(stakingTx, common.Hash{}, 0, 0, 0, true) if err != nil { utils.Logger().Debug(). Err(err). diff --git a/rpc/transaction.go b/rpc/transaction.go index 9497ba0a3..587991a7f 100644 --- a/rpc/transaction.go +++ b/rpc/transaction.go @@ -241,7 +241,7 @@ func (s *PublicTransactionService) GetStakingTransactionByHash( } return NewStructuredResponse(tx) case V2: - tx, err := v2.NewStakingTransaction(stx, blockHash, blockNumber, block.Time().Uint64(), index) + tx, err := v2.NewStakingTransaction(stx, blockHash, blockNumber, block.Time().Uint64(), index, true) if err != nil { return nil, err } diff --git a/rpc/v2/types.go b/rpc/v2/types.go index c66279426..bd3beb032 100644 --- a/rpc/v2/types.go +++ b/rpc/v2/types.go @@ -5,6 +5,8 @@ import ( "math/big" "strings" + "github.com/pkg/errors" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" ethtypes "github.com/ethereum/go-ethereum/core/types" @@ -126,6 +128,7 @@ type CreateValidatorMsg struct { SecurityContact string `json:"securityContact"` Details string `json:"details"` SlotPubKeys []bls.SerializedPublicKey `json:"slotPubKeys"` + SlotKeySigs []bls.SerializedSignature `json:"slotKeySigs"` } // EditValidatorMsg represents a staking transaction's edit validator directive that @@ -142,6 +145,7 @@ type EditValidatorMsg struct { Details string `json:"details"` SlotPubKeyToAdd *bls.SerializedPublicKey `json:"slotPubKeyToAdd"` SlotPubKeyToRemove *bls.SerializedPublicKey `json:"slotPubKeyToRemove"` + SlotKeyToAddSig *bls.SerializedSignature `json:"slotKeyToAddSig"` } // CollectRewardsMsg represents a staking transaction's collect rewards directive that @@ -419,12 +423,9 @@ func NewStakingTxReceipt( // representation, with the given location metadata set (if available). func NewStakingTransaction( tx *staking.StakingTransaction, blockHash common.Hash, - blockNumber uint64, timestamp uint64, index uint64, + blockNumber uint64, timestamp uint64, index uint64, signed bool, ) (*StakingTransaction, error) { - from, err := tx.SenderAddress() - if err != nil { - return nil, nil - } + v, r, s := tx.RawSignatureValues() var rpcMsg interface{} @@ -432,7 +433,7 @@ func NewStakingTransaction( case staking.DirectiveCreateValidator: rawMsg, err := staking.RLPDecodeStakeMsg(tx.Data(), staking.DirectiveCreateValidator) if err != nil { - return nil, err + return nil, errors.New(fmt.Sprintf("RLP decode error: %s", err.Error())) } msg, ok := rawMsg.(*staking.CreateValidator) if !ok { @@ -440,7 +441,7 @@ func NewStakingTransaction( } validatorAddress, err := internal_common.AddressToBech32(msg.ValidatorAddress) if err != nil { - return nil, err + return nil, errors.New(fmt.Sprintf("convert validator address error: %s", err.Error())) } rpcMsg = &CreateValidatorMsg{ ValidatorAddress: validatorAddress, @@ -456,11 +457,12 @@ func NewStakingTransaction( SecurityContact: msg.Description.SecurityContact, Details: msg.Description.Details, SlotPubKeys: msg.SlotPubKeys, + SlotKeySigs: msg.SlotKeySigs, } case staking.DirectiveEditValidator: rawMsg, err := staking.RLPDecodeStakeMsg(tx.Data(), staking.DirectiveEditValidator) if err != nil { - return nil, err + return nil, errors.New(fmt.Sprintf("RLP decode error: %s", err.Error())) } msg, ok := rawMsg.(*staking.EditValidator) if !ok { @@ -468,7 +470,7 @@ func NewStakingTransaction( } validatorAddress, err := internal_common.AddressToBech32(msg.ValidatorAddress) if err != nil { - return nil, err + return nil, errors.New(fmt.Sprintf("convert validator address error: %s", err.Error())) } // Edit validators txs need not have commission rates to edit commissionRate := &big.Int{} @@ -487,11 +489,12 @@ func NewStakingTransaction( Details: msg.Description.Details, SlotPubKeyToAdd: msg.SlotKeyToAdd, SlotPubKeyToRemove: msg.SlotKeyToRemove, + SlotKeyToAddSig: msg.SlotKeyToAddSig, } case staking.DirectiveCollectRewards: rawMsg, err := staking.RLPDecodeStakeMsg(tx.Data(), staking.DirectiveCollectRewards) if err != nil { - return nil, err + return nil, errors.New(fmt.Sprintf("RLP decode error: %s", err.Error())) } msg, ok := rawMsg.(*staking.CollectRewards) if !ok { @@ -499,13 +502,13 @@ func NewStakingTransaction( } delegatorAddress, err := internal_common.AddressToBech32(msg.DelegatorAddress) if err != nil { - return nil, err + return nil, errors.New(fmt.Sprintf("convert delegator address error: %s", err.Error())) } rpcMsg = &CollectRewardsMsg{DelegatorAddress: delegatorAddress} case staking.DirectiveDelegate: rawMsg, err := staking.RLPDecodeStakeMsg(tx.Data(), staking.DirectiveDelegate) if err != nil { - return nil, err + return nil, errors.New(fmt.Sprintf("RLP decode error: %s", err.Error())) } msg, ok := rawMsg.(*staking.Delegate) if !ok { @@ -513,11 +516,11 @@ func NewStakingTransaction( } delegatorAddress, err := internal_common.AddressToBech32(msg.DelegatorAddress) if err != nil { - return nil, err + return nil, errors.New(fmt.Sprintf("convert delegator address error: %s", err.Error())) } validatorAddress, err := internal_common.AddressToBech32(msg.ValidatorAddress) if err != nil { - return nil, err + return nil, errors.New(fmt.Sprintf("convert validator address error: %s", err.Error())) } rpcMsg = &DelegateMsg{ DelegatorAddress: delegatorAddress, @@ -527,7 +530,7 @@ func NewStakingTransaction( case staking.DirectiveUndelegate: rawMsg, err := staking.RLPDecodeStakeMsg(tx.Data(), staking.DirectiveUndelegate) if err != nil { - return nil, err + return nil, errors.New(fmt.Sprintf("RLP decode error: %s", err.Error())) } msg, ok := rawMsg.(*staking.Undelegate) if !ok { @@ -535,7 +538,7 @@ func NewStakingTransaction( } delegatorAddress, err := internal_common.AddressToBech32(msg.DelegatorAddress) if err != nil { - return nil, err + return nil, errors.New(fmt.Sprintf("convert delegator address error: %s", err.Error())) } validatorAddress, err := internal_common.AddressToBech32(msg.ValidatorAddress) if err != nil { @@ -566,11 +569,18 @@ func NewStakingTransaction( result.TransactionIndex = index } - fromAddr, err := internal_common.AddressToBech32(from) - if err != nil { - return nil, err + if signed { + from, err := tx.SenderAddress() + if err != nil { + return nil, errors.New(fmt.Sprintf("get sender address error: %s", err.Error())) + } + + fromAddr, err := internal_common.AddressToBech32(from) + if err != nil { + return nil, err + } + result.From = fromAddr } - result.From = fromAddr return result, nil } @@ -732,5 +742,5 @@ func NewStakingTransactionFromBlockIndex(b *types.Block, index uint64) (*Staking "tx index %v greater than or equal to number of transactions on block %v", index, b.Hash().String(), ) } - return NewStakingTransaction(txs[index], b.Hash(), b.NumberU64(), b.Time().Uint64(), index) + return NewStakingTransaction(txs[index], b.Hash(), b.NumberU64(), b.Time().Uint64(), index, true) } diff --git a/staking/types/transaction.go b/staking/types/transaction.go index 77671838f..ba93aefe6 100644 --- a/staking/types/transaction.go +++ b/staking/types/transaction.go @@ -143,6 +143,10 @@ func (tx *StakingTransaction) WithSignature(signer Signer, sig []byte) (*Staking return cpy, nil } +func (tx *StakingTransaction) SetRawSignature(v, r, s *big.Int) { + tx.data.R, tx.data.S, tx.data.V = r, s, v +} + // GasLimit returns gas of StakingTransaction. func (tx *StakingTransaction) GasLimit() uint64 { return tx.data.GasLimit diff --git a/test/helpers/transaction.go b/test/helpers/transaction.go index 835f3ccb9..11efd386f 100644 --- a/test/helpers/transaction.go +++ b/test/helpers/transaction.go @@ -34,7 +34,7 @@ func CreateTestStakingTransaction( // GetMessageFromStakingTx gets the staking message, as seen by the rpc layer func GetMessageFromStakingTx(tx *stakingTypes.StakingTransaction) (map[string]interface{}, error) { - rpcStakingTx, err := rpcV2.NewStakingTransaction(tx, ethcommon.Hash{}, 0, 0, 0) + rpcStakingTx, err := rpcV2.NewStakingTransaction(tx, ethcommon.Hash{}, 0, 0, 0, true) if err != nil { return nil, err }