diff --git a/.github/ISSUE_TEMPLATE/stake-heist-submission.md b/.github/ISSUE_TEMPLATE/stake-heist-submission.md deleted file mode 100644 index 458e015ce..000000000 --- a/.github/ISSUE_TEMPLATE/stake-heist-submission.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -name: stake heist submission -about: Submit work for Stake Heist Hackathon -title: "[submission]" -labels: stake-heist -assignees: gizemcakil - ---- - -# Linked bounty (please link the related bounty posting for your work) - -# Description of the bug, vulnerability, security threat OR the product/tool - -# Steps to reproduce the reported vulnerability OR steps taken to build the product/tool - -# Proof of exploitability OR proof of use-case (e.g. screenshot, video, github code source) - -# Perceived impact to another user or the organization - -# List of URLs and affected parameters (if applicable) - -# Browser, OS and/or app version used during testing (if applicable) diff --git a/.travis.yml b/.travis.yml index 20e20efbc..5fb9b65df 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ os: linux dist: bionic language: go go: - - 1.14 + - 1.16 go_import_path: github.com/harmony-one/harmony cache: directories: diff --git a/Dockerfile b/Dockerfile index 1038df164..a66a14d2e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ ENV BLS_DIR=${HMY_PATH}/bls ENV CGO_CFLAGS="-I${BLS_DIR}/include -I${MCL_DIR}/include" ENV CGO_LDFLAGS="-L${BLS_DIR}/lib" ENV LD_LIBRARY_PATH=${BLS_DIR}/lib:${MCL_DIR}/lib -ENV GIMME_GO_VERSION="1.14.1" +ENV GIMME_GO_VERSION="1.16.3" ENV PATH="/root/bin:${PATH}" RUN apt-get update -y diff --git a/README.md b/README.md index fd66e0500..67802c3f7 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Build Status](https://travis-ci.com/harmony-one/harmony.svg?branch=main)](https://travis-ci.com/harmony-one/harmony) ![gopherbadger-tag-do-not-edit](https://img.shields.io/badge/Go%20Coverage-45%25-brightgreen.svg?longCache=true&style=flat) ![Discord](https://img.shields.io/discord/532383335348043777.svg) -[![Coverage Status](https://coveralls.io/repos/github/harmony-one/harmony/badge.svg?branch=main)](https://coveralls.io/github/harmony-one/harmony?branch=master) +[![Github Action](https://github.com/harmony-one/harmony/actions/workflows/ci.yaml/badge.svg?event=push)](https://github.com/harmony-one/harmony/actions/workflows/ci.yaml) [![Go Report Card](https://goreportcard.com/badge/github.com/harmony-one/harmony)](https://goreportcard.com/report/github.com/harmony-one/harmony) ## General Documentation @@ -16,7 +16,7 @@ http://api.hmny.io/ ## Requirements -### **Go 1.14.7** +### **Go 1.16.3** ### **GMP and OpenSSL** On macOS: @@ -233,3 +233,4 @@ See [`CONTRIBUTING`](CONTRIBUTING.md) for details. - Integration with WASM - Fast state synchronization - Auditable privacy asset using ZK proof + diff --git a/consensus/consensus_service.go b/consensus/consensus_service.go index a5e93e53c..60f2fec56 100644 --- a/consensus/consensus_service.go +++ b/consensus/consensus_service.go @@ -12,7 +12,6 @@ import ( protobuf "github.com/golang/protobuf/proto" bls_core "github.com/harmony-one/bls/ffi/go/bls" msg_pb "github.com/harmony-one/harmony/api/proto/message" - "github.com/harmony-one/harmony/block" consensus_engine "github.com/harmony-one/harmony/consensus/engine" "github.com/harmony-one/harmony/consensus/quorum" "github.com/harmony-one/harmony/consensus/signature" @@ -242,49 +241,6 @@ func (consensus *Consensus) ReadSignatureBitmapPayload( ) } -// retrieve corresponding blsPublicKey from Coinbase Address -func (consensus *Consensus) getLeaderPubKeyFromCoinbase( - header *block.Header, -) (*bls.PublicKeyWrapper, error) { - shardState, err := consensus.Blockchain.ReadShardState(header.Epoch()) - if err != nil { - return nil, errors.Wrapf(err, "cannot read shard state %v %s", - header.Epoch(), - header.Coinbase().Hash().Hex(), - ) - } - - committee, err := shardState.FindCommitteeByID(header.ShardID()) - if err != nil { - return nil, err - } - - committerKey := new(bls_core.PublicKey) - isStaking := consensus.Blockchain.Config().IsStaking(header.Epoch()) - for _, member := range committee.Slots { - if isStaking { - // After staking the coinbase address will be the address of bls public key - if utils.GetAddressFromBLSPubKeyBytes(member.BLSPublicKey[:]) == header.Coinbase() { - if committerKey, err = bls.BytesToBLSPublicKey(member.BLSPublicKey[:]); err != nil { - return nil, err - } - return &bls.PublicKeyWrapper{Object: committerKey, Bytes: member.BLSPublicKey}, nil - } - } else { - if member.EcdsaAddress == header.Coinbase() { - if committerKey, err = bls.BytesToBLSPublicKey(member.BLSPublicKey[:]); err != nil { - return nil, err - } - return &bls.PublicKeyWrapper{Object: committerKey, Bytes: member.BLSPublicKey}, nil - } - } - } - return nil, errors.Errorf( - "cannot find corresponding BLS Public Key coinbase %s", - header.Coinbase().Hex(), - ) -} - // UpdateConsensusInformation will update shard information (epoch, publicKeys, blockNum, viewID) // based on the local blockchain. It is called in two cases for now: // 1. consensus object initialization. because of current dependency where chainreader is only available @@ -424,7 +380,7 @@ func (consensus *Consensus) UpdateConsensusInformation() Mode { // a solution to take care of this case because the coinbase of the latest block doesn't really represent the // the real current leader in case of M1 view change. if !curHeader.IsLastBlockInEpoch() && curHeader.Number().Uint64() != 0 { - leaderPubKey, err := consensus.getLeaderPubKeyFromCoinbase(curHeader) + leaderPubKey, err := chain.GetLeaderPubKeyFromCoinbase(consensus.Blockchain, curHeader) if err != nil || leaderPubKey == nil { consensus.getLogger().Error().Err(err). Msg("[UpdateConsensusInformation] Unable to get leaderPubKey from coinbase") @@ -481,15 +437,6 @@ func (consensus *Consensus) IsLeader() bool { return false } -// NeedsRandomNumberGeneration returns true if the current epoch needs random number generation -func (consensus *Consensus) NeedsRandomNumberGeneration(epoch *big.Int) bool { - if consensus.ShardID == 0 && epoch.Uint64() >= shard.Schedule.RandomnessStartingEpoch() { - return true - } - - return false -} - // SetViewIDs set both current view ID and view changing ID to the height // of the blockchain. It is used during client startup to recover the state func (consensus *Consensus) SetViewIDs(height uint64) { diff --git a/consensus/consensus_v2.go b/consensus/consensus_v2.go index f1c3c1ca5..06e8874aa 100644 --- a/consensus/consensus_v2.go +++ b/consensus/consensus_v2.go @@ -304,7 +304,6 @@ func (consensus *Consensus) Start( consensus.consensusTimeout[timeoutBootstrap].Start() consensus.getLogger().Info().Msg("[ConsensusMainLoop] Start bootstrap timeout (only once)") - vdfInProgress := false // Set up next block due time. consensus.NextBlockDue = time.Now().Add(consensus.BlockPeriod) start := false @@ -402,79 +401,6 @@ func (consensus *Consensus) Start( // Update time due for next block consensus.NextBlockDue = time.Now().Add(consensus.BlockPeriod) - //VRF/VDF is only generated in the beacon chain - if consensus.NeedsRandomNumberGeneration(newBlock.Header().Epoch()) { - // generate VRF if the current block has a new leader - if !consensus.Blockchain.IsSameLeaderAsPreviousBlock(newBlock) { - vrfBlockNumbers, err := consensus.Blockchain.ReadEpochVrfBlockNums(newBlock.Header().Epoch()) - if err != nil { - consensus.getLogger().Info(). - Uint64("MsgBlockNum", newBlock.NumberU64()). - Uint64("Epoch", newBlock.Header().Epoch().Uint64()). - Msg("[ConsensusMainLoop] no VRF block number from local db") - } - - //check if VRF is already generated for the current block - vrfAlreadyGenerated := false - for _, v := range vrfBlockNumbers { - if v == newBlock.NumberU64() { - consensus.getLogger().Info(). - Uint64("MsgBlockNum", newBlock.NumberU64()). - Uint64("Epoch", newBlock.Header().Epoch().Uint64()). - Msg("[ConsensusMainLoop] VRF is already generated for this block") - vrfAlreadyGenerated = true - break - } - } - - if !vrfAlreadyGenerated { - //generate a new VRF for the current block - vrfBlockNumbers := consensus.GenerateVrfAndProof(newBlock, vrfBlockNumbers) - - //generate a new VDF for the current epoch if there are enough VRFs in the current epoch - //note that >= instead of == is used, because it is possible the current leader - //can commit this block, go offline without finishing VDF - if (!vdfInProgress) && len(vrfBlockNumbers) >= consensus.VdfSeedSize() { - //check local database to see if there's a VDF generated for this epoch - //generate a VDF if no blocknum is available - _, err := consensus.Blockchain.ReadEpochVdfBlockNum(newBlock.Header().Epoch()) - if err != nil { - consensus.GenerateVdfAndProof(newBlock, vrfBlockNumbers) - vdfInProgress = true - } - } - } - } - - vdfOutput, seed, err := consensus.GetNextRnd() - if err == nil { - vdfInProgress = false - // Verify the randomness - vdfObject := vdf_go.New(shard.Schedule.VdfDifficulty(), seed) - if !vdfObject.Verify(vdfOutput) { - consensus.getLogger().Warn(). - Uint64("MsgBlockNum", newBlock.NumberU64()). - Uint64("Epoch", newBlock.Header().Epoch().Uint64()). - Msg("[ConsensusMainLoop] failed to verify the VDF output") - } else { - //write the VDF only if VDF has not been generated - _, err := consensus.Blockchain.ReadEpochVdfBlockNum(newBlock.Header().Epoch()) - if err == nil { - consensus.getLogger().Info(). - Uint64("MsgBlockNum", newBlock.NumberU64()). - Uint64("Epoch", newBlock.Header().Epoch().Uint64()). - Msg("[ConsensusMainLoop] VDF has already been generated previously") - } else { - consensus.getLogger().Info(). - Uint64("MsgBlockNum", newBlock.NumberU64()). - Uint64("Epoch", newBlock.Header().Epoch().Uint64()). - Msg("[ConsensusMainLoop] Generated a new VDF") - newBlock.AddVdf(vdfOutput[:]) - } - } - } - } - startTime = time.Now() consensus.msgSender.Reset(newBlock.NumberU64()) @@ -774,78 +700,34 @@ func (consensus *Consensus) postCatchup(initBN uint64) { } // GenerateVrfAndProof generates new VRF/Proof from hash of previous block -func (consensus *Consensus) GenerateVrfAndProof(newBlock *types.Block, vrfBlockNumbers []uint64) []uint64 { +func (consensus *Consensus) GenerateVrfAndProof(newHeader *block.Header) error { key, err := consensus.GetConsensusLeaderPrivateKey() if err != nil { - consensus.getLogger().Error(). - Err(err). - Msg("[GenerateVrfAndProof] VRF generation error") - return vrfBlockNumbers + return errors.New("[GenerateVrfAndProof] no leader private key provided") } sk := vrf_bls.NewVRFSigner(key.Pri) - blockHash := [32]byte{} - previousHeader := consensus.Blockchain.GetHeaderByNumber( - newBlock.NumberU64() - 1, - ) - if previousHeader == nil { - return vrfBlockNumbers - } - previousHash := previousHeader.Hash() - copy(blockHash[:], previousHash[:]) - - vrf, proof := sk.Evaluate(blockHash[:]) - newBlock.AddVrf(append(vrf[:], proof...)) - - consensus.getLogger().Info(). - Uint64("MsgBlockNum", newBlock.NumberU64()). - Uint64("Epoch", newBlock.Header().Epoch().Uint64()). - Int("Num of VRF", len(vrfBlockNumbers)). - Msg("[ConsensusMainLoop] Leader generated a VRF") - - return vrfBlockNumbers -} - -// ValidateVrfAndProof validates a VRF/Proof from hash of previous block -func (consensus *Consensus) ValidateVrfAndProof(headerObj *block.Header) bool { - vrfPk := vrf_bls.NewVRFVerifier(consensus.LeaderPubKey.Object) - var blockHash [32]byte previousHeader := consensus.Blockchain.GetHeaderByNumber( - headerObj.Number().Uint64() - 1, + newHeader.Number().Uint64() - 1, ) if previousHeader == nil { - return false + return errors.New("[GenerateVrfAndProof] no parent header found") } previousHash := previousHeader.Hash() - copy(blockHash[:], previousHash[:]) - vrfProof := [96]byte{} - copy(vrfProof[:], headerObj.Vrf()[32:]) - hash, err := vrfPk.ProofToHash(blockHash[:], vrfProof[:]) - - if err != nil { - consensus.getLogger().Warn(). - Err(err). - Str("MsgBlockNum", headerObj.Number().String()). - Msg("[OnAnnounce] VRF verification error") - return false + vrf, proof := sk.Evaluate(previousHash[:]) + if proof == nil { + return errors.New("[GenerateVrfAndProof] failed to generate vrf") } - if !bytes.Equal(hash[:], headerObj.Vrf()[:32]) { - consensus.getLogger().Warn(). - Str("MsgBlockNum", headerObj.Number().String()). - Msg("[OnAnnounce] VRF proof is not valid") - return false - } + newHeader.SetVrf(append(vrf[:], proof...)) - vrfBlockNumbers, _ := consensus.Blockchain.ReadEpochVrfBlockNums( - headerObj.Epoch(), - ) consensus.getLogger().Info(). - Str("MsgBlockNum", headerObj.Number().String()). - Int("Number of VRF", len(vrfBlockNumbers)). - Msg("[OnAnnounce] validated a new VRF") + Uint64("BlockNum", newHeader.Number().Uint64()). + Uint64("Epoch", newHeader.Epoch().Uint64()). + Hex("VRF+Proof", newHeader.Vrf()). + Msg("[GenerateVrfAndProof] Leader generated a VRF") - return true + return nil } // GenerateVdfAndProof generates new VDF/Proof from VRFs in the current epoch diff --git a/consensus/engine/consensus_engine.go b/consensus/engine/consensus_engine.go index a5b9c73d4..fc7bb4b02 100644 --- a/consensus/engine/consensus_engine.go +++ b/consensus/engine/consensus_engine.go @@ -105,6 +105,9 @@ type Engine interface { // VerifyShardState verifies the shard state during epoch transition is valid VerifyShardState(chain ChainReader, beacon ChainReader, header *block.Header) error + // VerifyVRF verifies the vrf of the block + VerifyVRF(chain ChainReader, header *block.Header) error + // Beaconchain provides the handle for Beaconchain Beaconchain() ChainReader diff --git a/consensus/view_change.go b/consensus/view_change.go index 9045f69ab..7938e3470 100644 --- a/consensus/view_change.go +++ b/consensus/view_change.go @@ -5,6 +5,8 @@ import ( "sync" "time" + "github.com/harmony-one/harmony/internal/chain" + "github.com/harmony-one/harmony/crypto/bls" "github.com/ethereum/go-ethereum/common" @@ -174,7 +176,7 @@ func (consensus *Consensus) getNextLeaderKey(viewID uint64) *bls.PublicKeyWrappe stuckBlockViewID := curHeader.ViewID().Uint64() + 1 gap = int(viewID - stuckBlockViewID) // this is the truth of the leader based on blockchain blocks - lastLeaderPubKey, err = consensus.getLeaderPubKeyFromCoinbase(curHeader) + lastLeaderPubKey, err = chain.GetLeaderPubKeyFromCoinbase(consensus.Blockchain, curHeader) if err != nil || lastLeaderPubKey == nil { consensus.getLogger().Error().Err(err). Msg("[getNextLeaderKey] Unable to get leaderPubKey from coinbase. Set it to consensus.LeaderPubKey") diff --git a/core/blockchain.go b/core/blockchain.go index 4f5b81815..d3fdcb257 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -2890,6 +2890,7 @@ func (bc *BlockChain) DelegatorsInformation(addr common.Address) []*staking.Dele } // GetECDSAFromCoinbase retrieve corresponding ecdsa address from Coinbase Address +// TODO: optimize this func by adding cache etc. func (bc *BlockChain) GetECDSAFromCoinbase(header *block.Header) (common.Address, error) { // backward compatibility: before isStaking epoch, coinbase address is the ecdsa address coinbase := header.Coinbase() diff --git a/core/evm.go b/core/evm.go index d2e1b73e2..c58a4f8c8 100644 --- a/core/evm.go +++ b/core/evm.go @@ -55,6 +55,11 @@ func NewEVMContext(msg Message, header *block.Header, chain ChainContext, author } else { beneficiary = *author } + vrf := common.Hash{} + if len(header.Vrf()) >= 32 { + vrfAndProof := header.Vrf() + copy(vrf[:], vrfAndProof[:32]) + } return vm.Context{ CanTransfer: CanTransfer, Transfer: Transfer, @@ -64,6 +69,7 @@ func NewEVMContext(msg Message, header *block.Header, chain ChainContext, author Coinbase: beneficiary, BlockNumber: header.Number(), EpochNumber: header.Epoch(), + VRF: vrf, Time: header.Time(), GasLimit: header.GasLimit(), GasPrice: new(big.Int).Set(msg.GasPrice()), diff --git a/core/rawdb/accessors_offchain.go b/core/rawdb/accessors_offchain.go index d3ee79d99..1789d917b 100644 --- a/core/rawdb/accessors_offchain.go +++ b/core/rawdb/accessors_offchain.go @@ -1,6 +1,7 @@ package rawdb import ( + "fmt" "math/big" "github.com/ethereum/go-ethereum/common" @@ -301,7 +302,7 @@ func ReadBlockCommitSig(db DatabaseReader, blockNum uint64) ([]byte, error) { // this is only needed for the compatibility in the migration moment. data, err = db.Get(lastCommitsKey) if err != nil { - return nil, errors.New("cannot read commit sig for block " + string(blockNum)) + return nil, errors.New(fmt.Sprintf("cannot read commit sig for block: %d ", blockNum)) } } return data, nil diff --git a/core/vm/contracts.go b/core/vm/contracts.go index 25a478275..2f5042804 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -76,6 +76,21 @@ var PrecompiledContractsIstanbul = map[common.Address]PrecompiledContract{ common.BytesToAddress([]byte{9}): &blake2F{}, } +// PrecompiledContractsIstanbul contains the default set of pre-compiled Ethereum +// contracts used in the Istanbul release. +var PrecompiledContractsVRF = map[common.Address]PrecompiledContract{ + common.BytesToAddress([]byte{1}): &ecrecover{}, + common.BytesToAddress([]byte{2}): &sha256hash{}, + common.BytesToAddress([]byte{3}): &ripemd160hash{}, + common.BytesToAddress([]byte{4}): &dataCopy{}, + common.BytesToAddress([]byte{5}): &bigModExp{}, + common.BytesToAddress([]byte{6}): &bn256AddIstanbul{}, + common.BytesToAddress([]byte{7}): &bn256ScalarMulIstanbul{}, + common.BytesToAddress([]byte{8}): &bn256PairingIstanbul{}, + common.BytesToAddress([]byte{9}): &blake2F{}, + common.BytesToAddress([]byte{255}): &vrf{}, +} + // RunPrecompiledContract runs and evaluates the output of a precompiled contract. func RunPrecompiledContract(p PrecompiledContract, input []byte, contract *Contract) (ret []byte, err error) { gas := p.RequiredGas(input) @@ -501,3 +516,20 @@ func (c *blake2F) Run(input []byte) ([]byte, error) { } return output, nil } + +// VRF implemented as a native contract. +type vrf struct{} + +// RequiredGas returns the gas required to execute the pre-compiled contract. +// +// This method does not require any overflow checking as the input size gas costs +// required for anything significant is so high it's impossible to pay for. +func (c *vrf) RequiredGas(input []byte) uint64 { + return GasQuickStep +} + +func (c *vrf) Run(input []byte) ([]byte, error) { + // Note the input was overwritten with the vrf of the block. + // So here we simply return it + return append([]byte{}, input...), nil +} diff --git a/core/vm/evm.go b/core/vm/evm.go index d2f6e8a94..080e5febd 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -54,7 +54,14 @@ func run(evm *EVM, contract *Contract, input []byte, readOnly bool) ([]byte, err if evm.chainRules.IsIstanbul { precompiles = PrecompiledContractsIstanbul } + if evm.chainRules.IsVRF { + precompiles = PrecompiledContractsVRF + } if p := precompiles[*contract.CodeAddr]; p != nil { + if _, ok := p.(*vrf); ok { + // Override the input with vrf data of the current block so it can be returned to the contract program. + input = evm.Context.VRF.Bytes() + } return RunPrecompiledContract(p, input, contract) } } @@ -69,6 +76,7 @@ func run(evm *EVM, contract *Contract, input []byte, readOnly bool) ([]byte, err evm.interpreter = interpreter } return interpreter.Run(contract, input, readOnly) + } } return nil, ErrNoCompatibleInterpreter @@ -99,6 +107,7 @@ type Context struct { BlockNumber *big.Int // Provides information for NUMBER EpochNumber *big.Int // Provides information for EPOCH Time *big.Int // Provides information for TIME + VRF common.Hash // Provides information for VRF TxType types.TransactionType } @@ -225,6 +234,10 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas if evm.chainRules.IsIstanbul { precompiles = PrecompiledContractsIstanbul } + if evm.chainRules.IsVRF { + precompiles = PrecompiledContractsVRF + } + if precompiles[addr] == nil && evm.ChainConfig().IsS3(evm.EpochNumber) && value.Sign() == 0 { // Calling a non existing account, don't do anything, but ping the tracer if evm.vmConfig.Debug && evm.depth == 0 { diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index 0682a1c84..a50babdc0 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -271,6 +271,7 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) ( // execute the operation res, err = operation.execute(&pc, in, contract, mem, stack) + // verifyPool is a build flag. Pool verification makes sure the integrity // of the integer pool by comparing values to a default value. if verifyPool { diff --git a/core/vm/runtime/env.go b/core/vm/runtime/env.go index 8b59d8edb..3fa00ceb2 100644 --- a/core/vm/runtime/env.go +++ b/core/vm/runtime/env.go @@ -34,6 +34,7 @@ func NewEnv(cfg *Config) *vm.EVM { Coinbase: cfg.Coinbase, BlockNumber: cfg.BlockNumber, EpochNumber: cfg.EpochNumber, + VRF: cfg.VRF, Time: cfg.Time, GasLimit: cfg.GasLimit, GasPrice: cfg.GasPrice, diff --git a/core/vm/runtime/runtime.go b/core/vm/runtime/runtime.go index 4f67349ee..8ebba4030 100644 --- a/core/vm/runtime/runtime.go +++ b/core/vm/runtime/runtime.go @@ -38,6 +38,7 @@ type Config struct { Coinbase common.Address BlockNumber *big.Int EpochNumber *big.Int + VRF common.Hash Time *big.Int GasLimit uint64 GasPrice *big.Int diff --git a/crypto/vrf/bls/bls_vrf.go b/crypto/vrf/bls/bls_vrf.go index f81fbacf2..54e1820c7 100644 --- a/crypto/vrf/bls/bls_vrf.go +++ b/crypto/vrf/bls/bls_vrf.go @@ -65,6 +65,9 @@ func (k *PrivateKey) Evaluate(alpha []byte) ([32]byte, []byte) { //pi = VRF_prove(SK, alpha) msgHash := sha256.Sum256(alpha) pi := k.SignHash(msgHash[:]) + if pi == nil { + return [32]byte{}, nil + } //hash the signature and output as VRF beta //beta = VRF_proof2hash(pi) diff --git a/go.mod b/go.mod index fecd3f132..2c39d4c28 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/harmony-one/harmony -go 1.14 +go 1.16 require ( github.com/VictoriaMetrics/fastcache v1.5.7 // indirect @@ -17,7 +17,7 @@ require ( github.com/ethereum/go-ethereum v1.9.23 github.com/fjl/memsize v0.0.0-20180929194037-2a09253e352a // indirect github.com/garslo/gogen v0.0.0-20170307003452-d6ebae628c7c // indirect - github.com/golang/mock v1.4.0 + github.com/golang/mock v1.4.4 github.com/golang/protobuf v1.4.3 github.com/golangci/golangci-lint v1.22.2 github.com/gorilla/mux v1.8.0 @@ -29,14 +29,14 @@ require ( github.com/hashicorp/go-version v1.2.0 github.com/hashicorp/golang-lru v0.5.4 github.com/ipfs/go-ds-badger v0.2.4 - github.com/libp2p/go-libp2p v0.13.0 - github.com/libp2p/go-libp2p-core v0.8.0 + github.com/libp2p/go-libp2p v0.14.0 + github.com/libp2p/go-libp2p-core v0.8.5 github.com/libp2p/go-libp2p-crypto v0.1.0 github.com/libp2p/go-libp2p-discovery v0.5.0 github.com/libp2p/go-libp2p-kad-dht v0.11.1 github.com/libp2p/go-libp2p-pubsub v0.4.0 github.com/multiformats/go-multiaddr v0.3.1 - github.com/multiformats/go-multiaddr-dns v0.2.0 + github.com/multiformats/go-multiaddr-dns v0.3.1 github.com/natefinch/lumberjack v2.0.0+incompatible github.com/pborman/uuid v1.2.0 github.com/pelletier/go-toml v1.2.0 @@ -49,16 +49,16 @@ require ( github.com/spf13/cobra v0.0.5 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.6.1 - github.com/stretchr/testify v1.6.1 + github.com/stretchr/testify v1.7.0 github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca go.uber.org/ratelimit v0.1.0 - golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a + golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 golang.org/x/lint v0.0.0-20200302205851-738671d3881b - golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 - golang.org/x/tools v0.0.0-20200904185747-39188db58858 - google.golang.org/grpc v1.31.1 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c + golang.org/x/tools v0.0.0-20210106214847-113979e3529a + google.golang.org/grpc v1.33.2 google.golang.org/protobuf v1.25.0 - gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/olebedev/go-duktape.v3 v3.0.0-20200619000410-60c24ae608a6 gopkg.in/yaml.v2 v2.3.0 diff --git a/hmy/downloader/adapter_test.go b/hmy/downloader/adapter_test.go index 540381589..f2ce5f3a5 100644 --- a/hmy/downloader/adapter_test.go +++ b/hmy/downloader/adapter_test.go @@ -134,6 +134,9 @@ func (e *dummyEngine) VerifyHeaderSignature(engine.ChainReader, *block.Header, b func (e *dummyEngine) VerifyCrossLink(engine.ChainReader, types.CrossLink) error { return nil } +func (e *dummyEngine) VerifyVRF(chain engine.ChainReader, header *block.Header) error { + return nil +} func (e *dummyEngine) VerifyHeaders(engine.ChainReader, []*block.Header, []bool) (chan<- struct{}, <-chan error) { return nil, nil } diff --git a/internal/chain/engine.go b/internal/chain/engine.go index d1894f1c9..e0c34994a 100644 --- a/internal/chain/engine.go +++ b/internal/chain/engine.go @@ -5,6 +5,9 @@ import ( "math/big" "sort" + bls2 "github.com/harmony-one/bls/ffi/go/bls" + blsvrf "github.com/harmony-one/harmony/crypto/vrf/bls" + "github.com/ethereum/go-ethereum/common" lru "github.com/hashicorp/golang-lru" "github.com/pkg/errors" @@ -29,6 +32,8 @@ import ( const ( verifiedSigCache = 100 epochCtxCache = 20 + vrfBeta = 32 // 32 bytes randomness + vrfProof = 96 // 96 bytes proof (bls sig) ) type engineImpl struct { @@ -135,6 +140,104 @@ func (e *engineImpl) VerifyShardState( return nil } +// VerifyVRF implements Engine, checking the vrf is valid +func (e *engineImpl) VerifyVRF( + bc engine.ChainReader, header *block.Header, +) error { + if bc.ShardID() != header.ShardID() { + return errors.Errorf( + "[VerifyVRF] shardID not match %d %d", bc.ShardID(), header.ShardID(), + ) + } + + if bc.Config().IsVRF(header.Epoch()) && len(header.Vrf()) != vrfBeta+vrfProof { + return errors.Errorf( + "[VerifyVRF] invalid vrf data format or no vrf proposed %x", header.Vrf(), + ) + } + if !bc.Config().IsVRF(header.Epoch()) { + if len(header.Vrf()) != 0 { + return errors.Errorf( + "[VerifyVRF] vrf data present in pre-vrf epoch %x", header.Vrf(), + ) + } + return nil + } + + leaderPubKey, err := GetLeaderPubKeyFromCoinbase(bc, header) + + if leaderPubKey == nil || err != nil { + return err + } + + vrfPk := blsvrf.NewVRFVerifier(leaderPubKey.Object) + + previousHeader := bc.GetHeaderByNumber( + header.Number().Uint64() - 1, + ) + if previousHeader == nil { + return errors.New("[VerifyVRF] no parent header found") + } + + previousHash := previousHeader.Hash() + vrfProof := [vrfProof]byte{} + copy(vrfProof[:], header.Vrf()[vrfBeta:]) + hash, err := vrfPk.ProofToHash(previousHash[:], vrfProof[:]) + + if err != nil { + return errors.New("[VerifyVRF] Failed VRF verification") + } + + if !bytes.Equal(hash[:], header.Vrf()[:vrfBeta]) { + return errors.New("[VerifyVRF] VRF proof is not valid") + } + + return nil +} + +// retrieve corresponding blsPublicKey from Coinbase Address +func GetLeaderPubKeyFromCoinbase( + blockchain engine.ChainReader, h *block.Header, +) (*bls.PublicKeyWrapper, error) { + shardState, err := blockchain.ReadShardState(h.Epoch()) + if err != nil { + return nil, errors.Wrapf(err, "cannot read shard state %v %s", + h.Epoch(), + h.Coinbase().Hash().Hex(), + ) + } + + committee, err := shardState.FindCommitteeByID(h.ShardID()) + if err != nil { + return nil, err + } + + committerKey := new(bls2.PublicKey) + isStaking := blockchain.Config().IsStaking(h.Epoch()) + for _, member := range committee.Slots { + if isStaking { + // After staking the coinbase address will be the address of bls public key + if utils.GetAddressFromBLSPubKeyBytes(member.BLSPublicKey[:]) == h.Coinbase() { + if committerKey, err = bls.BytesToBLSPublicKey(member.BLSPublicKey[:]); err != nil { + return nil, err + } + return &bls.PublicKeyWrapper{Object: committerKey, Bytes: member.BLSPublicKey}, nil + } + } else { + if member.EcdsaAddress == h.Coinbase() { + if committerKey, err = bls.BytesToBLSPublicKey(member.BLSPublicKey[:]); err != nil { + return nil, err + } + return &bls.PublicKeyWrapper{Object: committerKey, Bytes: member.BLSPublicKey}, nil + } + } + } + return nil, errors.Errorf( + "cannot find corresponding BLS Public Key coinbase %s", + h.Coinbase().Hex(), + ) +} + // VerifySeal implements Engine, checking whether the given block's parent block satisfies // the PoS difficulty requirements, i.e. >= 2f+1 valid signatures from the committee // Note that each block header contains the bls signature of the parent block diff --git a/internal/configs/sharding/fixedschedule.go b/internal/configs/sharding/fixedschedule.go index d5fa9b8d9..1576f9321 100644 --- a/internal/configs/sharding/fixedschedule.go +++ b/internal/configs/sharding/fixedschedule.go @@ -41,12 +41,6 @@ func (s fixedSchedule) VdfDifficulty() int { return mainnetVdfDifficulty } -// TODO: remove it after randomness feature turned on mainnet -//RandonnessStartingEpoch returns starting epoch of randonness generation -func (s fixedSchedule) RandomnessStartingEpoch() uint64 { - return mainnetRandomnessStartingEpoch -} - func (s fixedSchedule) GetNetworkID() NetworkID { return DevNet } diff --git a/internal/configs/sharding/localnet.go b/internal/configs/sharding/localnet.go index 927ec0f83..a434451f9 100644 --- a/internal/configs/sharding/localnet.go +++ b/internal/configs/sharding/localnet.go @@ -24,8 +24,6 @@ const ( localnetBlocksPerEpochV2 = 10 localnetVdfDifficulty = 5000 // This takes about 10s to finish the vdf - - localnetRandomnessStartingEpoch = 0 ) func (ls localnetSchedule) InstanceForEpoch(epoch *big.Int) Instance { @@ -116,12 +114,6 @@ func (ls localnetSchedule) VdfDifficulty() int { return localnetVdfDifficulty } -// TODO: remove it after randomness feature turned on mainnet -//RandonnessStartingEpoch returns starting epoch of randonness generation -func (ls localnetSchedule) RandomnessStartingEpoch() uint64 { - return localnetRandomnessStartingEpoch -} - func (ls localnetSchedule) GetNetworkID() NetworkID { return LocalNet } diff --git a/internal/configs/sharding/mainnet.go b/internal/configs/sharding/mainnet.go index 04cb4684a..6b82b10c2 100644 --- a/internal/configs/sharding/mainnet.go +++ b/internal/configs/sharding/mainnet.go @@ -17,9 +17,6 @@ const ( mainnetVdfDifficulty = 50000 // This takes about 100s to finish the vdf - // TODO: remove it after randomness feature turned on mainnet - mainnetRandomnessStartingEpoch = 100000 - mainnetV0_1Epoch = 1 mainnetV0_2Epoch = 5 mainnetV0_3Epoch = 8 @@ -176,12 +173,6 @@ func (ms mainnetSchedule) VdfDifficulty() int { return mainnetVdfDifficulty } -// TODO: remove it after randomness feature turned on mainnet -//RandonnessStartingEpoch returns starting epoch of randonness generation -func (ms mainnetSchedule) RandomnessStartingEpoch() uint64 { - return mainnetRandomnessStartingEpoch -} - func (ms mainnetSchedule) GetNetworkID() NetworkID { return MainNet } diff --git a/internal/configs/sharding/pangaea.go b/internal/configs/sharding/pangaea.go index baa2a8b81..ac0bdf5bd 100644 --- a/internal/configs/sharding/pangaea.go +++ b/internal/configs/sharding/pangaea.go @@ -56,12 +56,6 @@ func (ps pangaeaSchedule) VdfDifficulty() int { return pangaeaVdfDifficulty } -// TODO: remove it after randomness feature turned on mainnet -//RandonnessStartingEpoch returns starting epoch of randonness generation -func (ps pangaeaSchedule) RandomnessStartingEpoch() uint64 { - return mainnetRandomnessStartingEpoch -} - func (ps pangaeaSchedule) GetNetworkID() NetworkID { return Pangaea } diff --git a/internal/configs/sharding/partner.go b/internal/configs/sharding/partner.go index 9ff31f413..98836966b 100644 --- a/internal/configs/sharding/partner.go +++ b/internal/configs/sharding/partner.go @@ -57,12 +57,6 @@ func (ps partnerSchedule) VdfDifficulty() int { return partnerVdfDifficulty } -// TODO: remove it after randomness feature turned on mainnet -//RandonnessStartingEpoch returns starting epoch of randonness generation -func (ps partnerSchedule) RandomnessStartingEpoch() uint64 { - return mainnetRandomnessStartingEpoch -} - func (ps partnerSchedule) GetNetworkID() NetworkID { return Partner } diff --git a/internal/configs/sharding/shardingconfig.go b/internal/configs/sharding/shardingconfig.go index e18095e2f..1e6122e1d 100644 --- a/internal/configs/sharding/shardingconfig.go +++ b/internal/configs/sharding/shardingconfig.go @@ -30,10 +30,6 @@ type Schedule interface { // VDFDifficulty returns number of iterations for VDF calculation VdfDifficulty() int - // TODO: remove it after randomness feature turned on mainnet - //RandomnessStartingEpoch returns starting epoch of randonness generation - RandomnessStartingEpoch() uint64 - // GetNetworkID() return networkID type. GetNetworkID() NetworkID diff --git a/internal/configs/sharding/stress.go b/internal/configs/sharding/stress.go index 6b04b3a52..f98e664ab 100644 --- a/internal/configs/sharding/stress.go +++ b/internal/configs/sharding/stress.go @@ -59,12 +59,6 @@ func (ss stressnetSchedule) VdfDifficulty() int { return stressnetVdfDifficulty } -// TODO: remove it after randomness feature turned on mainnet -//RandonnessStartingEpoch returns starting epoch of randonness generation -func (ss stressnetSchedule) RandomnessStartingEpoch() uint64 { - return mainnetRandomnessStartingEpoch -} - func (ss stressnetSchedule) GetNetworkID() NetworkID { return StressNet } diff --git a/internal/configs/sharding/testnet.go b/internal/configs/sharding/testnet.go index d6e5f7ea5..1e332da86 100644 --- a/internal/configs/sharding/testnet.go +++ b/internal/configs/sharding/testnet.go @@ -94,12 +94,6 @@ func (ts testnetSchedule) VdfDifficulty() int { return testnetVdfDifficulty } -// TODO: remove it after randomness feature turned on mainnet -//RandonnessStartingEpoch returns starting epoch of randonness generation -func (ts testnetSchedule) RandomnessStartingEpoch() uint64 { - return mainnetRandomnessStartingEpoch -} - func (ts testnetSchedule) GetNetworkID() NetworkID { return TestNet } diff --git a/internal/params/config.go b/internal/params/config.go index 907f2175b..69b0ee7a7 100644 --- a/internal/params/config.go +++ b/internal/params/config.go @@ -50,6 +50,7 @@ var ( SixtyPercentEpoch: big.NewInt(530), // Around Monday Apr 12th 2021, 22:30 UTC RedelegationEpoch: big.NewInt(290), NoEarlyUnlockEpoch: big.NewInt(530), // Around Monday Apr 12th 2021, 22:30 UTC + VRFEpoch: EpochTBD, EIP155Epoch: big.NewInt(28), S3Epoch: big.NewInt(28), IstanbulEpoch: big.NewInt(314), @@ -72,6 +73,7 @@ var ( SixtyPercentEpoch: big.NewInt(73282), RedelegationEpoch: big.NewInt(36500), NoEarlyUnlockEpoch: big.NewInt(73580), + VRFEpoch: EpochTBD, EIP155Epoch: big.NewInt(0), S3Epoch: big.NewInt(0), IstanbulEpoch: big.NewInt(43800), @@ -95,6 +97,7 @@ var ( SixtyPercentEpoch: big.NewInt(0), RedelegationEpoch: big.NewInt(0), NoEarlyUnlockEpoch: big.NewInt(0), + VRFEpoch: big.NewInt(0), EIP155Epoch: big.NewInt(0), S3Epoch: big.NewInt(0), IstanbulEpoch: big.NewInt(0), @@ -118,6 +121,7 @@ var ( SixtyPercentEpoch: big.NewInt(0), RedelegationEpoch: big.NewInt(0), NoEarlyUnlockEpoch: big.NewInt(0), + VRFEpoch: big.NewInt(0), EIP155Epoch: big.NewInt(0), S3Epoch: big.NewInt(0), IstanbulEpoch: big.NewInt(0), @@ -141,6 +145,7 @@ var ( SixtyPercentEpoch: big.NewInt(10), RedelegationEpoch: big.NewInt(0), NoEarlyUnlockEpoch: big.NewInt(0), + VRFEpoch: big.NewInt(0), EIP155Epoch: big.NewInt(0), S3Epoch: big.NewInt(0), IstanbulEpoch: big.NewInt(0), @@ -163,6 +168,7 @@ var ( SixtyPercentEpoch: EpochTBD, // Never enable it for localnet as localnet has no external validator setup RedelegationEpoch: big.NewInt(0), NoEarlyUnlockEpoch: big.NewInt(0), + VRFEpoch: big.NewInt(0), EIP155Epoch: big.NewInt(0), S3Epoch: big.NewInt(0), IstanbulEpoch: big.NewInt(0), @@ -187,6 +193,7 @@ var ( big.NewInt(0), // SixtyPercentEpoch big.NewInt(0), // RedelegationEpoch big.NewInt(0), // NoEarlyUnlockEpoch + big.NewInt(0), // VRFEpoch big.NewInt(0), // EIP155Epoch big.NewInt(0), // S3Epoch big.NewInt(0), // IstanbulEpoch @@ -211,6 +218,7 @@ var ( big.NewInt(0), // SixtyPercentEpoch big.NewInt(0), // RedelegationEpoch big.NewInt(0), // NoEarlyUnlockEpoch + big.NewInt(0), // VRFEpoch big.NewInt(0), // EIP155Epoch big.NewInt(0), // S3Epoch big.NewInt(0), // IstanbulEpoch @@ -288,6 +296,9 @@ type ChainConfig struct { // more than 7 epochs is disabled NoEarlyUnlockEpoch *big.Int `json:"no-early-unlock-epoch,omitempty"` + // VRFEpoch is the epoch when VRF randomness is enabled + VRFEpoch *big.Int `json:"vrf-epoch,omitempty"` + // EIP155 hard fork epoch (include EIP158 too) EIP155Epoch *big.Int `json:"eip155-epoch,omitempty"` @@ -374,6 +385,11 @@ func (c *ChainConfig) IsNoEarlyUnlock(epoch *big.Int) bool { return isForked(c.NoEarlyUnlockEpoch, epoch) } +// IsVRF determines whether it is the epoch to enable vrf +func (c *ChainConfig) IsVRF(epoch *big.Int) bool { + return isForked(c.VRFEpoch, epoch) +} + // IsPreStaking determines whether staking transactions are allowed func (c *ChainConfig) IsPreStaking(epoch *big.Int) bool { return isForked(c.PreStakingEpoch, epoch) @@ -452,9 +468,9 @@ func isForked(s, epoch *big.Int) bool { // Rules is a one time interface meaning that it shouldn't be used in between transition // phases. type Rules struct { - ChainID *big.Int - EthChainID *big.Int - IsCrossLink, IsEIP155, IsS3, IsReceiptLog, IsIstanbul bool + ChainID *big.Int + EthChainID *big.Int + IsCrossLink, IsEIP155, IsS3, IsReceiptLog, IsIstanbul, IsVRF bool } // Rules ensures c's ChainID is not nil. @@ -475,5 +491,6 @@ func (c *ChainConfig) Rules(epoch *big.Int) Rules { IsS3: c.IsS3(epoch), IsReceiptLog: c.IsReceiptLog(epoch), IsIstanbul: c.IsIstanbul(epoch), + IsVRF: c.IsVRF(epoch), } } diff --git a/node/node_handler.go b/node/node_handler.go index a6a1684e6..ec027c0df 100644 --- a/node/node_handler.go +++ b/node/node_handler.go @@ -277,6 +277,18 @@ func (node *Node) VerifyNewBlock(newBlock *types.Block) error { return err } + if err := node.Blockchain().Engine().VerifyVRF( + node.Blockchain(), newBlock.Header(), + ); err != nil { + utils.Logger().Error(). + Str("blockHash", newBlock.Hash().Hex()). + Err(err). + Msg("[VerifyNewBlock] Cannot verify vrf for the new block") + return errors.Wrap(err, + "[VerifyNewBlock] Cannot verify vrf for the new block", + ) + } + if err := node.Blockchain().Engine().VerifyShardState( node.Blockchain(), node.Beaconchain(), newBlock.Header(), ); err != nil { @@ -284,7 +296,7 @@ func (node *Node) VerifyNewBlock(newBlock *types.Block) error { Str("blockHash", newBlock.Hash().Hex()). Err(err). Msg("[VerifyNewBlock] Cannot verify shard state for the new block") - return errors.New( + return errors.Wrap(err, "[VerifyNewBlock] Cannot verify shard state for the new block", ) } diff --git a/node/node_handler_test.go b/node/node_handler_test.go index 13dde23a4..eb76f1f50 100644 --- a/node/node_handler_test.go +++ b/node/node_handler_test.go @@ -1,6 +1,7 @@ package node import ( + "math/big" "testing" "github.com/ethereum/go-ethereum/common" @@ -102,7 +103,79 @@ func TestVerifyNewBlock(t *testing.T) { commitSigs, func() uint64 { return 0 }, common.Address{}, nil, nil, ) + // work around vrf verification as it's tested in another test. + node.Blockchain().Config().VRFEpoch = big.NewInt(2) if err := node.VerifyNewBlock(block); err != nil { t.Error("New block is not verified successfully:", err) } } + +func TestVerifyVRF(t *testing.T) { + blsKey := bls.RandPrivateKey() + pubKey := blsKey.GetPublicKey() + leader := p2p.Peer{IP: "127.0.0.1", Port: "8882", ConsensusPubKey: pubKey} + priKey, _, _ := utils.GenKeyP2P("127.0.0.1", "9902") + host, err := p2p.NewHost(p2p.HostConfig{ + Self: &leader, + BLSKey: priKey, + }) + if err != nil { + t.Fatalf("newhost failure: %v", err) + } + decider := quorum.NewDecider( + quorum.SuperMajorityVote, shard.BeaconChainShardID, + ) + consensus, err := consensus.New( + host, shard.BeaconChainShardID, leader, multibls.GetPrivateKeys(blsKey), decider, + ) + if err != nil { + t.Fatalf("Cannot craeate consensus: %v", err) + } + archiveMode := make(map[uint32]bool) + archiveMode[0] = true + archiveMode[1] = false + node := New(host, consensus, testDBFactory, nil, archiveMode) + + consensus.Blockchain = node.Blockchain() + txs := make(map[common.Address]types.Transactions) + stks := staking.StakingTransactions{} + node.Worker.CommitTransactions( + txs, stks, common.Address{}, + ) + commitSigs := make(chan []byte) + go func() { + commitSigs <- []byte{} + }() + + ecdsaAddr := pubKey.GetAddress() + + shardState := &shard.State{} + com := shard.Committee{ShardID: uint32(0)} + + spKey := bls.SerializedPublicKey{} + spKey.FromLibBLSPublicKey(pubKey) + curNodeID := shard.Slot{ + ecdsaAddr, + spKey, + nil, + } + com.Slots = append(com.Slots, curNodeID) + shardState.Epoch = big.NewInt(1) + shardState.Shards = append(shardState.Shards, com) + + node.Consensus.LeaderPubKey = &bls.PublicKeyWrapper{spKey, pubKey} + node.Worker.GetCurrentHeader().SetEpoch(big.NewInt(1)) + node.Consensus.GenerateVrfAndProof(node.Worker.GetCurrentHeader()) + block, _ := node.Worker.FinalizeNewBlock( + commitSigs, func() uint64 { return 0 }, ecdsaAddr, nil, shardState, + ) + // Write shard state for the new epoch + node.Blockchain().WriteShardStateBytes(node.Blockchain().ChainDb(), big.NewInt(1), node.Worker.GetCurrentHeader().ShardState()) + + node.Blockchain().Config().VRFEpoch = big.NewInt(0) + if err := node.Blockchain().Engine().VerifyVRF( + node.Blockchain(), block.Header(), + ); err != nil { + t.Error("New vrf is not verified successfully:", err) + } +} diff --git a/node/node_newblock.go b/node/node_newblock.go index 21b4b220b..c29d8cca2 100644 --- a/node/node_newblock.go +++ b/node/node_newblock.go @@ -164,6 +164,14 @@ func (node *Node) ProposeNewBlock(commitSigs chan []byte) (*types.Block, error) return nil, err } + // Add VRF + if node.Blockchain().Config().IsVRF(header.Epoch()) { + //generate a new VRF for the current block + if err := node.Consensus.GenerateVrfAndProof(header); err != nil { + return nil, err + } + } + // Prepare normal and staking transactions retrieved from transaction pool utils.AnalysisStart("proposeNewBlockChooseFromTxnPool") diff --git a/node/node_syncing.go b/node/node_syncing.go index 89ef503e4..2ce127ce0 100644 --- a/node/node_syncing.go +++ b/node/node_syncing.go @@ -87,8 +87,15 @@ func (node *Node) createStateSync() *legacysync.StateSync { // Thus for compatibility, we are doing the arithmetics here, and not to change the // protocol itself. This is just the temporary hack and will not be a concern after // state sync. - syncPort := node.downloaderServer.Port - mutatedPort := strconv.Itoa(syncPort + legacysync.SyncingPortDifference) + var mySyncPort int + if node.downloaderServer != nil { + mySyncPort = node.downloaderServer.Port + } else { + // If local sync server is not started, the port field in protocol is actually not + // functional, simply set it to default value. + mySyncPort = nodeconfig.DefaultDNSPort + } + mutatedPort := strconv.Itoa(mySyncPort + legacysync.SyncingPortDifference) return legacysync.CreateStateSync(node.SelfPeer.IP, mutatedPort, node.GetSyncID(), node.NodeConfig.Role() == nodeconfig.ExplorerNode) } diff --git a/p2p/stream/common/requestmanager/requestmanager.go b/p2p/stream/common/requestmanager/requestmanager.go index 42a8d897f..886e3d23a 100644 --- a/p2p/stream/common/requestmanager/requestmanager.go +++ b/p2p/stream/common/requestmanager/requestmanager.go @@ -92,7 +92,7 @@ func (rm *requestManager) DoRequest(ctx context.Context, raw sttypes.Request, op func (rm *requestManager) doRequestAsync(ctx context.Context, raw sttypes.Request, options ...RequestOption) <-chan responseData { req := &request{ Request: raw, - respC: make(chan responseData), + respC: make(chan responseData, 1), doneC: make(chan struct{}), } for _, opt := range options { 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/infra/Dockerfile b/rosetta/infra/Dockerfile index 7dd0b00ed..d0c929490 100644 --- a/rosetta/infra/Dockerfile +++ b/rosetta/infra/Dockerfile @@ -1,5 +1,5 @@ # Build -FROM golang:1.14 AS build +FROM golang:1.16 AS build RUN apt update -y && \ apt install libgmp-dev libssl-dev git -y && \ 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/scripts/travis_go_checker.sh b/scripts/travis_go_checker.sh index c98b2a43c..2ecadaa13 100755 --- a/scripts/travis_go_checker.sh +++ b/scripts/travis_go_checker.sh @@ -80,6 +80,8 @@ else fi echo "Running go test..." +# Fix https://github.com/golang/go/issues/44129#issuecomment-788351567 +go get -t ./... if go test -v -count=1 ./... then echo "go test succeeded." 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/chain/vrf/main.go b/test/chain/vrf/main.go new file mode 100644 index 000000000..bbe3caffd --- /dev/null +++ b/test/chain/vrf/main.go @@ -0,0 +1,73 @@ +package main + +import ( + "bytes" + "fmt" + "time" + + "github.com/harmony-one/harmony/crypto/bls" + + bls_core "github.com/harmony-one/bls/ffi/go/bls" + "github.com/harmony-one/harmony/crypto/hash" + vrf_bls "github.com/harmony-one/harmony/crypto/vrf/bls" +) + +func init() { + bls_core.Init(bls_core.BLS12_381) +} + +func main() { + blsPriKey := bls.RandPrivateKey() + pubKeyWrapper := bls.PublicKeyWrapper{Object: blsPriKey.GetPublicKey()} + pubKeyWrapper.Bytes.FromLibBLSPublicKey(pubKeyWrapper.Object) + + blockHash := hash.Keccak256([]byte{1, 2, 3, 4, 5}) + + sig := &bls_core.Sign{} + startTime := time.Now() + for i := 0; i < 1000; i++ { + sig = blsPriKey.SignHash(blockHash[:]) + } + endTime := time.Now() + fmt.Printf("Time required to sign 1000 times: %f seconds\n", endTime.Sub(startTime).Seconds()) + + startTime = time.Now() + for i := 0; i < 1000; i++ { + sig.VerifyHash(pubKeyWrapper.Object, blockHash[:]) + } + endTime = time.Now() + fmt.Printf("Time required to verify sig 1000 times: %f seconds\n", endTime.Sub(startTime).Seconds()) + + sk := vrf_bls.NewVRFSigner(blsPriKey) + + vrf := [32]byte{} + proof := []byte{} + + startTime = time.Now() + for i := 0; i < 1000; i++ { + vrf, proof = sk.Evaluate(blockHash[:]) + } + endTime = time.Now() + fmt.Printf("Time required to generate vrf 1000 times: %f seconds\n", endTime.Sub(startTime).Seconds()) + + pk := vrf_bls.NewVRFVerifier(blsPriKey.GetPublicKey()) + + resultVrf := [32]byte{} + startTime = time.Now() + for i := 0; i < 1000; i++ { + resultVrf, _ = pk.ProofToHash(blockHash, proof) + } + endTime = time.Now() + fmt.Printf("Time required to verify vrf 1000 times: %f seconds\n", endTime.Sub(startTime).Seconds()) + + if bytes.Compare(vrf[:], resultVrf[:]) != 0 { + fmt.Printf("Failed to verify VRF") + + } + + // A example result of a single run: + //Time required to sign 1000 times: 0.542673 seconds + //Time required to verify sig 1000 times: 1.499797 seconds + //Time required to generate vrf 1000 times: 0.525362 seconds + //Time required to verify vrf 1000 times: 2.076890 seconds +} 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 }