From 07c6d0f685ce006bc02a6e30da1b16b0e6b93de8 Mon Sep 17 00:00:00 2001 From: Prasanna Loganathar Date: Tue, 22 Aug 2023 04:44:57 +0800 Subject: [PATCH] Cleanup token migration on attributes (#2356) * Cleanup token migration on attributes * Cleanup token migration on attributes * Cleanup token symbol errors * Update version * Heuristic based grouping * Add helpful note * Fix indentation * Remove extra indentation * Improve logs * Remove unused enumerate * Revert "EVM: Discard dst20_contract error (#2355)" This reverts commit 04ac6498bbfed804e2d4078242b1057755f03beb. * Use genesis block flag * Cleanup genesis block * Restore comments * Fix Queue error logging * Test correct deployed bytecode * Fix evmQueueId type --------- Co-authored-by: Peter John Bushnell Co-authored-by: Niven Co-authored-by: jouzo Co-authored-by: Jouzo <15011228+Jouzo@users.noreply.github.com> --- lib/ain-evm/src/evm.rs | 31 +++++--- lib/ain-evm/src/lib.rs | 2 +- make.sh | 10 ++- src/masternodes/govvariables/attributes.cpp | 25 ++---- src/masternodes/mn_checks.cpp | 23 +----- src/masternodes/tokens.cpp | 59 +++++++++----- src/masternodes/tokens.h | 4 +- src/masternodes/validation.cpp | 5 +- src/rpc/blockchain.cpp | 84 ++++++++++---------- src/version.h | 17 ++-- test/functional/feature_dst20.py | 16 +++- test/functional/feature_loan_setloantoken.py | 2 +- test/functional/feature_token_split.py | 2 +- test/functional/feature_tokens_basic.py | 2 +- 14 files changed, 152 insertions(+), 130 deletions(-) diff --git a/lib/ain-evm/src/evm.rs b/lib/ain-evm/src/evm.rs index e83294c8d8..b129fac600 100644 --- a/lib/ain-evm/src/evm.rs +++ b/lib/ain-evm/src/evm.rs @@ -134,12 +134,6 @@ impl EVMServices { let tx_queue = self.core.tx_queues.get(queue_id)?; let mut queue = tx_queue.data.lock().unwrap(); - let is_evm_genesis_block = queue.target_block == U256::zero(); - if is_evm_genesis_block { - let migration_txs = get_dst20_migration_txs(mnview_ptr)?; - queue.transactions.extend(migration_txs.into_iter()) - } - let queue_txs_len = queue.transactions.len(); let mut all_transactions = Vec::with_capacity(queue_txs_len); let mut failed_transactions = Vec::with_capacity(queue_txs_len); @@ -194,12 +188,15 @@ impl EVMServices { let mut executor = AinExecutor::new(&mut backend); - // Ensure that state root changes by updating counter contract storage - if current_block_number == U256::zero() { + let is_evm_genesis_block = queue.target_block == U256::zero(); + if is_evm_genesis_block { // reserve DST20 namespace self.reserve_dst20_namespace(&mut executor)?; - // Deploy contract on the first block + let migration_txs = get_dst20_migration_txs(mnview_ptr)?; + queue.transactions.extend(migration_txs.into_iter()); + + // Deploy counter contract on the first block let DeployContractInfo { address, storage, @@ -207,12 +204,14 @@ impl EVMServices { } = EVMServices::counter_contract(dvm_block_number, current_block_number)?; executor.deploy_contract(address, bytecode, storage)?; } else { + // Ensure that state root changes by updating counter contract storage let DeployContractInfo { address, storage, .. } = EVMServices::counter_contract(dvm_block_number, current_block_number)?; executor.update_storage(address, storage)?; } - for (_idx, queue_item) in queue.transactions.clone().into_iter().enumerate() { + + for queue_item in queue.transactions.clone() { match queue_item.tx { QueueTx::SignedTx(signed_tx) => { let nonce = executor.get_nonce(&signed_tx.sender); @@ -505,7 +504,7 @@ impl EVMServices { None => {} Some(account) => { if account.code_hash != ain_contracts::get_system_reserved_codehash()? { - debug!("Token address is already in use for {name} {symbol}"); + return Err(format_err!("Token address is already in use").into()); } } } @@ -633,7 +632,10 @@ impl EVMServices { .collect::>(); for address in addresses { - debug!("Deploying address to {:#?}", address); + debug!( + "[reserve_dst20_namespace] Deploying address to {:#?}", + address + ); executor.deploy_contract(address, bytecode.clone().into(), Vec::new())?; } @@ -668,7 +670,10 @@ fn get_dst20_migration_txs(mnview_ptr: usize) -> Result> { let mut txs = Vec::new(); for token in ain_cpp_imports::get_dst20_tokens(mnview_ptr) { let address = ain_contracts::dst20_address_from_token_id(token.id)?; - debug!("Deploying to address {:#?}", address); + debug!( + "[get_dst20_migration_txs] Deploying to address {:#?}", + address + ); let tx = QueueTx::SystemTx(SystemTx::DeployContract(DeployContractData { name: token.name, diff --git a/lib/ain-evm/src/lib.rs b/lib/ain-evm/src/lib.rs index 243b015605..d163eef3d6 100644 --- a/lib/ain-evm/src/lib.rs +++ b/lib/ain-evm/src/lib.rs @@ -34,7 +34,7 @@ pub type MaybeTransactionV2 = Option; pub enum EVMError { #[error("EVM: Backend error: {0:?}")] TrieCreationFailed(#[from] BackendError), - #[error("EVM: Queue error")] + #[error("EVM: Queue error {0:?}")] QueueError(#[from] QueueError), #[error("EVM: Queue invalid nonce error {0:?}")] QueueInvalidNonce((Box, ethereum_types::U256)), diff --git a/make.sh b/make.sh index 6ea00e3974..c06a7f45b1 100755 --- a/make.sh +++ b/make.sh @@ -63,7 +63,7 @@ setup_vars() { MAKE_DEPS_ARGS=${MAKE_DEPS_ARGS:-} TESTS_FAILFAST=${TESTS_FAILFAST:-"0"} TESTS_COMBINED_LOGS=${TESTS_COMBINED_LOGS:-"0"} - CI_GROUP_LOGS=${CI_GROUP_LOGS:-"1"} + CI_GROUP_LOGS=${CI_GROUP_LOGS:-"$(get_default_ci_group_logs)"} } main() { @@ -950,6 +950,14 @@ get_default_use_clang() { return } +get_default_ci_group_logs() { + if [[ -n "${GITHUB_ACTIONS-}" ]]; then + echo 1 + else + echo 0 + fi +} + # Dev tools # --- diff --git a/src/masternodes/govvariables/attributes.cpp b/src/masternodes/govvariables/attributes.cpp index 89be581691..a0387ab3aa 100644 --- a/src/masternodes/govvariables/attributes.cpp +++ b/src/masternodes/govvariables/attributes.cpp @@ -4,7 +4,6 @@ #include #include -#include #include /// CAccountsHistoryWriter #include /// DeFiErrors @@ -16,6 +15,7 @@ #include /// GetDecimaleString #include /// ValueFromAmount #include +#include enum class EVMAttributesTypes : uint32_t { Finalized = 1, @@ -1810,25 +1810,6 @@ Res ATTRIBUTES::Validate(const CCustomCSView &view) const { if (GetValue(intervalPriceKey, CTokenCurrencyPair{}) == CTokenCurrencyPair{}) { return DeFiErrors::GovVarValidateCurrencyPair(); } - - const CDataStructureV0 enabledKey{AttributeTypes::Param, ParamIDs::Feature, - DFIPKeys::EVMEnabled}; - - CrossBoundaryResult result; - if (view.GetLastHeight() >= Params().GetConsensus().NextNetworkUpgradeHeight && - GetValue(enabledKey, false) && - evmQueueId && - !evm_try_is_dst20_deployed_or_queued(result, evmQueueId, token->name, token->symbol, - tokenID.v)) { - evm_try_create_dst20(result, evmQueueId, token->creationTx.GetHex(), - token->name, - token->symbol, - tokenID.v); - - if (!result.ok) { - return DeFiErrors::GovVarErrorCreatingDST20(result.reason.c_str()); - } - } break; } case TokenKeys::FixedIntervalPriceId: @@ -2313,6 +2294,7 @@ Res ATTRIBUTES::Apply(CCustomCSView &mnview, const uint32_t height) { return DeFiErrors::GovVarUnsupportedValue(); } + // TODO: Cut this out. CrossBoundaryResult result; if (!evm_try_set_attribute(result, evmQueueId, attributeType, *number)) { return DeFiErrors::SettingEVMAttributeFailure(); @@ -2322,6 +2304,9 @@ Res ATTRIBUTES::Apply(CCustomCSView &mnview, const uint32_t height) { } } } + + // TODO: evm_try_handle_attribute_apply here. + // Pass the whole apply chain. On the rust side, pick and choose what needs to be handled return Res::Ok(); } diff --git a/src/masternodes/mn_checks.cpp b/src/masternodes/mn_checks.cpp index ae65635b44..aa8111fc89 100644 --- a/src/masternodes/mn_checks.cpp +++ b/src/masternodes/mn_checks.cpp @@ -1057,32 +1057,17 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor { token.creationTx = tx.GetHash(); token.creationHeight = height; - // check foundation auth if (token.IsDAT() && !HasFoundationAuth()) { return Res::Err("tx not from foundation member"); } - if (static_cast(height) >= consensus.BayfrontHeight) { // formal compatibility if someone cheat and create - // LPS token on the pre-bayfront node + if (static_cast(height) >= consensus.BayfrontHeight) { if (token.IsPoolShare()) { return Res::Err("Can't manually create 'Liquidity Pool Share' token; use poolpair creation"); } } - auto tokenId = mnview.CreateToken(token, static_cast(height) < consensus.BayfrontHeight); - - if (tokenId && token.IsDAT() && isEvmEnabledForBlock) { - CrossBoundaryResult result; - evm_try_create_dst20(result, evmQueueId, tx.GetHash().GetHex(), - rust::string(tokenName.c_str()), - rust::string(tokenSymbol.c_str()), - tokenId->v); - - if (!result.ok) { - return Res::Err("Error creating DST20 token: %s", result.reason); - } - } - + auto tokenId = mnview.CreateToken(token, static_cast(height) < consensus.BayfrontHeight, isEvmEnabledForBlock, evmQueueId); return tokenId; } @@ -1435,7 +1420,7 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor { token.creationTx = tx.GetHash(); token.creationHeight = height; - auto tokenId = mnview.CreateToken(token); + auto tokenId = mnview.CreateToken(token, false, false, evmQueueId); Require(tokenId); rewards = obj.rewards; @@ -2651,7 +2636,7 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor { token.flags |= static_cast(CToken::TokenFlags::LoanToken) | static_cast(CToken::TokenFlags::DAT); - auto tokenId = mnview.CreateToken(token); + auto tokenId = mnview.CreateToken(token, false, isEvmEnabledForBlock, evmQueueId); Require(tokenId); if (height >= static_cast(consensus.FortCanningCrunchHeight) && IsTokensMigratedToGovVar()) { diff --git a/src/masternodes/tokens.cpp b/src/masternodes/tokens.cpp index 6c8db2c703..0c1aa0e047 100644 --- a/src/masternodes/tokens.cpp +++ b/src/masternodes/tokens.cpp @@ -9,6 +9,9 @@ #include #include #include +#include +#include +#include #include @@ -61,17 +64,19 @@ Res CTokensView::CreateDFIToken() { return Res::Ok(); } -ResVal CTokensView::CreateToken(const CTokensView::CTokenImpl &token, bool isPreBayfront) { - // this should not happen, but for sure - Require(!GetTokenByCreationTx(token.creationTx), - [=]{ return strprintf("token with creation tx %s already exists!", - token.creationTx.ToString()); }); - - Require(token.IsValidSymbol()); +ResVal CTokensView::CreateToken(const CTokensView::CTokenImpl &token, bool isPreBayfront, bool shouldCreateDst20, uint64_t evmQueueId) { + if (GetTokenByCreationTx(token.creationTx)) { + return Res::Err("token with creation tx %s already exists!", token.creationTx.ToString()); + } + if (auto r = token.IsValidSymbol(); !r) { + return r; + } DCT_ID id{0}; if (token.IsDAT()) { - Require(!GetToken(token.symbol), [=]{ return strprintf("token '%s' already exists!", token.symbol); }); + if (GetToken(token.symbol)) { + return Res::Err("token '%s' already exists!", token.symbol); + } ForEachToken( [&](DCT_ID const ¤tId, CLazySerialize) { @@ -81,21 +86,30 @@ ResVal CTokensView::CreateToken(const CTokensView::CTokenImpl &token, bo }, id); if (id == DCT_ID_START) { - // asserted before BayfrontHeight, keep it for strict sameness - Require( - !isPreBayfront, - []{ return "Critical fault: trying to create DCT_ID same as DCT_ID_START for Foundation owner\n"; }); + if (isPreBayfront) { + return Res::Err("Critical fault: trying to create DCT_ID same as DCT_ID_START for Foundation owner\n"); + } id = IncrementLastDctId(); LogPrintf("Warning! Range (id, token); WriteBy(symbolKey, id); WriteBy(token.creationTx, id); @@ -231,15 +245,24 @@ std::optional CTokensView::ReadLastDctId() const { if (Read(LastDctId::prefix(), lastDctId)) { return {lastDctId}; } - return {}; } inline Res CTokenImplementation::IsValidSymbol() const { - Require(!symbol.empty() && !IsDigit(symbol[0]), []{ return "token symbol should be non-empty and starts with a letter"; }); - Require(symbol.find('#') == std::string::npos, []{ return "token symbol should not contain '#'"; }); + auto invalidTokenSymbol = []() { + return Res::Err("Invalid token symbol. Valid: Start with an alphabet, non-empty, not contain # or /"); + }; + + if (symbol.empty() || IsDigit(symbol[0])) { + return invalidTokenSymbol(); + } + if (symbol.find('#') != std::string::npos) { + return invalidTokenSymbol(); + } if (creationHeight >= Params().GetConsensus().FortCanningCrunchHeight) { - Require(symbol.find('/') == std::string::npos, []{ return "token symbol should not contain '/'"; }); + if (symbol.find('/') != std::string::npos) { + return invalidTokenSymbol(); + }; } return Res::Ok(); } diff --git a/src/masternodes/tokens.h b/src/masternodes/tokens.h index 5bbc544f34..6595c6b661 100644 --- a/src/masternodes/tokens.h +++ b/src/masternodes/tokens.h @@ -123,8 +123,8 @@ class CTokensView : public virtual CStorageView { DCT_ID const &start = DCT_ID{0}); Res CreateDFIToken(); - ResVal CreateToken(const CTokenImpl &token, bool isPreBayfront = false); - Res UpdateToken(const CTokenImpl &newToken, bool isPreBayfront = false, const bool tokenSplitUpdatea = false); + ResVal CreateToken(const CTokenImpl &token, bool isPreBayfront = false, bool shouldCreateDst20 = false, uint64_t evmQueueId = 0); + Res UpdateToken(const CTokenImpl &newToken, bool isPreBayfront = false, const bool tokenSplitUpdate = false); Res BayfrontFlagsCleanup(); Res AddMintedTokens(DCT_ID const &id, const CAmount &amount); diff --git a/src/masternodes/validation.cpp b/src/masternodes/validation.cpp index 82b23cf907..4a50c92ba4 100644 --- a/src/masternodes/validation.cpp +++ b/src/masternodes/validation.cpp @@ -1270,7 +1270,7 @@ static Res PoolSplits(CCustomCSView& view, CAmount& totalBalance, ATTRIBUTES& at throw std::runtime_error(res.msg); } - auto resVal = view.CreateToken(newPoolToken); + auto resVal = view.CreateToken(newPoolToken, false, false); if (!resVal) { throw std::runtime_error(resVal.msg); } @@ -1809,7 +1809,8 @@ static void ProcessTokenSplits(const CBlock& block, const CBlockIndex* pindex, C continue; } - auto resVal = view.CreateToken(newToken); + // TODO: Pass this on, once we add support for EVM splits + auto resVal = view.CreateToken(newToken, false, false, 0); if (!resVal) { LogPrintf("Token split failed on CreateToken %s\n", resVal.msg); continue; diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index 9eabe391ca..d43b43cded 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -269,54 +269,54 @@ struct RewardInfo { std::optional VmInfoUniv(const CTransaction& tx) { auto evmBlockHeaderToUniValue = [](const EVMBlockHeader& header) { - UniValue r(UniValue::VOBJ); - r.pushKV("parenthash", std::string(header.parent_hash.data(), header.parent_hash.length())); - r.pushKV("beneficiary", std::string(header.beneficiary.data(), header.beneficiary.length())); - r.pushKV("stateRoot", std::string(header.state_root.data(), header.state_root.length())); - r.pushKV("receiptRoot", std::string(header.receipts_root.data(), header.receipts_root.length())); - r.pushKV("number", header.number); - r.pushKV("gasLimit", header.gas_limit); - r.pushKV("gasUsed", header.gas_used); - r.pushKV("timestamp", header.timestamp); - r.pushKV("nonce", header.nonce); - r.pushKV("baseFee", header.base_fee); - return r; + UniValue r(UniValue::VOBJ); + r.pushKV("parenthash", std::string(header.parent_hash.data(), header.parent_hash.length())); + r.pushKV("beneficiary", std::string(header.beneficiary.data(), header.beneficiary.length())); + r.pushKV("stateRoot", std::string(header.state_root.data(), header.state_root.length())); + r.pushKV("receiptRoot", std::string(header.receipts_root.data(), header.receipts_root.length())); + r.pushKV("number", header.number); + r.pushKV("gasLimit", header.gas_limit); + r.pushKV("gasUsed", header.gas_used); + r.pushKV("timestamp", header.timestamp); + r.pushKV("nonce", header.nonce); + r.pushKV("baseFee", header.base_fee); + return r; }; - CustomTxType guess; - UniValue txResults(UniValue::VOBJ); - if (tx.IsCoinBase()) { - if (tx.vout.size() < 2) { - // TODO: Decode vout 0 to dvm - return {}; - } - auto tx1ScriptPubKey = tx.vout[1].scriptPubKey; - if (tx1ScriptPubKey.size() == 0) return {}; - auto xvm = XVM::TryFrom(tx1ScriptPubKey); - if (!xvm) return {}; - UniValue result(UniValue::VOBJ); - result.pushKV("vmtype", "coinbase"); - result.pushKV("txtype", "coinbase"); - result.pushKV("msg", xvm->ToUniValue()); - CrossBoundaryResult res; - auto evmBlockHeader = evm_try_get_block_header_by_hash(res, xvm->evm.blockHash); - if (!res.ok) return {}; - result.pushKV("xvmHeader", evmBlockHeaderToUniValue(evmBlockHeader)); - return result; - } - auto res = RpcInfo(tx, std::numeric_limits::max(), guess, txResults); - if (guess == CustomTxType::None) { + CustomTxType guess; + UniValue txResults(UniValue::VOBJ); + if (tx.IsCoinBase()) { + if (tx.vout.size() < 2) { + // TODO: Decode vout 0 to dvm return {}; } + auto tx1ScriptPubKey = tx.vout[1].scriptPubKey; + if (tx1ScriptPubKey.size() == 0) return {}; + auto xvm = XVM::TryFrom(tx1ScriptPubKey); + if (!xvm) return {}; UniValue result(UniValue::VOBJ); - result.pushKV("vmtype", guess == CustomTxType::EvmTx ? "evm" : "dvm"); - result.pushKV("txtype", ToString(guess)); - if (!res.ok) { - result.pushKV("error", res.msg); - } else { - result.pushKV("msg", txResults); - } + result.pushKV("vmtype", "coinbase"); + result.pushKV("txtype", "coinbase"); + result.pushKV("msg", xvm->ToUniValue()); + CrossBoundaryResult res; + auto evmBlockHeader = evm_try_get_block_header_by_hash(res, xvm->evm.blockHash); + if (!res.ok) return {}; + result.pushKV("xvmHeader", evmBlockHeaderToUniValue(evmBlockHeader)); return result; + } + auto res = RpcInfo(tx, std::numeric_limits::max(), guess, txResults); + if (guess == CustomTxType::None) { + return {}; + } + UniValue result(UniValue::VOBJ); + result.pushKV("vmtype", guess == CustomTxType::EvmTx ? "evm" : "dvm"); + result.pushKV("txtype", ToString(guess)); + if (!res.ok) { + result.pushKV("error", res.msg); + } else { + result.pushKV("msg", txResults); + } + return result; } UniValue ExtendedTxToUniv(const CTransaction& tx, bool include_hex, int serialize_flags, int version, bool txDetails) { diff --git a/src/version.h b/src/version.h index beab01c3d6..4b8dc213e0 100644 --- a/src/version.h +++ b/src/version.h @@ -9,16 +9,23 @@ * network protocol versioning */ -static const int PROTOCOL_VERSION = 70037; - //! initial proto version, to be increased after version/verack negotiation static const int INIT_PROTO_VERSION = 209; -//! In this version, 'getheaders' was introduced. -static const int GETHEADERS_VERSION = 31800; +// TODO: Alias this to the latest network upgrade height that the node supports +// as they are synonymous. +static const int PROTOCOL_VERSION = 70037; +// TODO: Set this as the previous successful network upgrade height +// pre-node release. Each version can supports connecting from +// N (oldest = last upgrade) to PROTOCOL_VERSION (latest) +// //! disconnect from peers older than this proto version -static const int MIN_PEER_PROTO_VERSION = 70036; +static const int MIN_PEER_PROTO_VERSION = 70023; + + +//! In this version, 'getheaders' was introduced. +static const int GETHEADERS_VERSION = 31800; //! nTime field added to CAddress, starting with this version; //! if possible, avoid requesting addresses nodes older than this diff --git a/test/functional/feature_dst20.py b/test/functional/feature_dst20.py index baad9a0f22..07dcc8cd32 100755 --- a/test/functional/feature_dst20.py +++ b/test/functional/feature_dst20.py @@ -15,7 +15,6 @@ from test_framework.test_framework import DefiTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error - class DST20(DefiTestFramework): def set_test_params(self): self.num_nodes = 1 @@ -138,7 +137,7 @@ def test_dst20_migration_txs(self): self.nodes[0].w3.to_hex( self.nodes[0].w3.eth.get_code(self.contract_address_usdt) ), - tx1["input"], + self.bytecode, ) # check BTC migration @@ -159,7 +158,7 @@ def test_dst20_migration_txs(self): self.nodes[0].w3.to_hex( self.nodes[0].w3.eth.get_code(self.contract_address_btc) ), - tx2["input"], + self.bytecode, ) # check ETH migration @@ -180,7 +179,7 @@ def test_dst20_migration_txs(self): self.nodes[0].w3.to_hex( self.nodes[0].w3.eth.get_code(self.contract_address_eth) ), - tx3["input"], + self.bytecode, ) assert_equal(tx1["input"], tx2["input"]) @@ -801,6 +800,15 @@ def run_test(self): ).read() )["object"] + self.bytecode = json.loads( + open( + f"{os.path.dirname(__file__)}/../../lib/ain-contracts/dst20/output/bytecode.json", + "r", + encoding="utf8", + ).read() + )["object"] + + # Generate chain self.node.generate(150) self.nodes[0].utxostoaccount({self.address: "1000@DFI"}) diff --git a/test/functional/feature_loan_setloantoken.py b/test/functional/feature_loan_setloantoken.py index a8e3a54f80..fdbcb0a7e7 100755 --- a/test/functional/feature_loan_setloantoken.py +++ b/test/functional/feature_loan_setloantoken.py @@ -192,7 +192,7 @@ def run_test(self): assert_raises_rpc_error( -32600, - "token symbol should be non-empty and starts with a letter", + "Invalid token symbol", self.nodes[0].setloantoken, { "symbol": "", diff --git a/test/functional/feature_token_split.py b/test/functional/feature_token_split.py index e9f58dbb97..a9da4497e7 100755 --- a/test/functional/feature_token_split.py +++ b/test/functional/feature_token_split.py @@ -630,7 +630,7 @@ def token_split(self): # Make sure we cannot make a token with '/' in its symbol assert_raises_rpc_error( -32600, - "token symbol should not contain '/'", + "Invalid token symbol", self.nodes[0].createtoken, {"symbol": "bad/v1", "collateralAddress": self.address}, ) diff --git a/test/functional/feature_tokens_basic.py b/test/functional/feature_tokens_basic.py index 08854a8aaa..0a660c535a 100755 --- a/test/functional/feature_tokens_basic.py +++ b/test/functional/feature_tokens_basic.py @@ -68,7 +68,7 @@ def run_test(self): ) except JSONRPCException as e: errorString = e.error["message"] - assert "token symbol should not contain '#'" in errorString + assert "Invalid token symbol" in errorString print("Create token 'GOLD' (128)...") createTokenTx = self.nodes[0].createtoken(