Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions contracts/sysio.msgch/sysio.msgch.abi
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,18 @@
{
"name": "ATTESTATION_TYPE_EMISSIONS_BLOCKED",
"value": 60962
},
{
"name": "ATTESTATION_TYPE_SYNDICATE_LIQ",
"value": 60963
},
{
"name": "ATTESTATION_TYPE_LIQ_YIELD",
"value": 60964
},
{
"name": "ATTESTATION_TYPE_DESYNDICATE_LIQ",
"value": 60965
}
]
},
Expand Down
Binary file modified contracts/sysio.msgch/sysio.msgch.wasm
Binary file not shown.
49 changes: 34 additions & 15 deletions contracts/sysio.roa/sysio.roa.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,28 @@ namespace sysio {
// literals -- a contract rename is one change here, not scattered across call sites).
constexpr name AUTHEX_ACCOUNT = "sysio.authex"_n;
constexpr name AUTHEX_RECORDLINK = "recordlink"_n;

/// Maximum number of generated account names checked before newuser gives up.
constexpr uint32_t MAX_ACCOUNT_NAME_ATTEMPTS{100};

/// SplitMix64's Weyl-sequence increment, used to decorrelate account-name generator inputs.
constexpr uint64_t ACCOUNT_NAME_MIX_INCREMENT{0x9E3779B97F4A7C15ULL};
/// First SplitMix64 avalanche multiplier.
constexpr uint64_t ACCOUNT_NAME_MIX_MULTIPLIER_1{0xBF58476D1CE4E5B9ULL};
/// Second SplitMix64 avalanche multiplier.
constexpr uint64_t ACCOUNT_NAME_MIX_MULTIPLIER_2{0x94D049BB133111EBULL};
/// SplitMix64's three avalanche shifts, in application order.
constexpr uint32_t ACCOUNT_NAME_MIX_SHIFT_1{30};
constexpr uint32_t ACCOUNT_NAME_MIX_SHIFT_2{27};
constexpr uint32_t ACCOUNT_NAME_MIX_SHIFT_3{31};

/// Applies the SplitMix64 finalizer to one account-name generator input.
uint64_t mix_account_name_seed(uint64_t value) {
value += ACCOUNT_NAME_MIX_INCREMENT;
value = (value ^ (value >> ACCOUNT_NAME_MIX_SHIFT_1)) * ACCOUNT_NAME_MIX_MULTIPLIER_1;
value = (value ^ (value >> ACCOUNT_NAME_MIX_SHIFT_2)) * ACCOUNT_NAME_MIX_MULTIPLIER_2;
return value ^ (value >> ACCOUNT_NAME_MIX_SHIFT_3);
}
} // anonymous namespace

static bool is_sysio_account(const name& account) {
Expand Down Expand Up @@ -1032,7 +1054,7 @@ namespace sysio {
check(prefix_len + 2 <= NAME_LENGTH, "Creator name is too long to generate a sub-account under it");
size_t gen_len = NAME_LENGTH - prefix_len - 1; // chars after "<prefix>."

// Try up to 3 times to generate a unique username
// Try a bounded number of times to generate a unique username.
name new_username;
bool created = false;
uint32_t block_num = current_block_number();
Expand All @@ -1050,23 +1072,17 @@ namespace sysio {
'p','q','r','s','t','u','v','w','x','y','z'};
constexpr size_t charmap_len = sizeof(charmap) / sizeof(charmap[0]);

// Cheap pseudo-random generator: a splitmix64 finalizer over nonce/attempt/block_num. No
// crypto is needed here — uniqueness is enforced by the is_account retry below; we only
// need variation — so this avoids a sha256 intrinsic call per attempt.
auto mix = [](uint64_t z) {
z += 0x9E3779B97F4A7C15ULL;
z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL;
z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL;
return z ^ (z >> 31);
};
// Mix the block number before combining it with the nonce. A linear shifted combination
// lets related name-valued nonces in different blocks produce the same candidate sequence.
// No cryptographic strength is needed: is_account enforces uniqueness below.
const uint64_t seed = nonce.value ^ mix_account_name_seed(static_cast<uint64_t>(block_num));

for (uint8_t attempt = 0; attempt < 3; ++attempt) {
uint64_t x = nonce.value ^ (static_cast<uint64_t>(block_num) << 32)
^ (static_cast<uint64_t>(attempt) * 0x9E3779B97F4A7C15ULL);
for (uint32_t attempt = 0; attempt < MAX_ACCOUNT_NAME_ATTEMPTS; ++attempt) {
uint64_t x = seed ^ (static_cast<uint64_t>(attempt) * ACCOUNT_NAME_MIX_INCREMENT);

// Fill the generated portion after "<prefix>."
for (size_t i = 0; i < gen_len; ++i) {
x = mix(x);
x = mix_account_name_seed(x);
uname_str[prefix_len + 1 + i] = charmap[x % charmap_len];
}

Expand All @@ -1078,7 +1094,10 @@ namespace sysio {
break;
}
}
check(created, "Failed to generate a unique account name after 3 attempts");
check(created, [] {
return "Failed to generate a unique account name after " +
std::to_string(MAX_ACCOUNT_NAME_ATTEMPTS) + " attempts";
});

auto owner_auth = sysiosystem::authority{1, {{pubkey, 1}}, {}};
auto active_auth = sysiosystem::authority{1, {{pubkey, 1}}, {}};
Expand Down
4 changes: 3 additions & 1 deletion contracts/sysio.roa/sysio.roa.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,9 @@ namespace sysio {
* - The action will create a new account utilizing a new randomly generated username with the provided `pubkey` as its authority.
* - The registration count for the `creator` will be incremented.
* - The sponsor mapping (creator, nonce -> username) will be recorded in the sponsors table.
* - The action will fail if the `creator` is not a tier-1 node owner or if the account already exists.
* - The action fails if ROA is inactive, the `creator` is not a tier-1 node owner, the
* creator name cannot fit a generated suffix, the nonce was already used by that creator,
* or no unused generated name is found within the bounded candidate search.
*
* ### Rights Granted
* - The new user account is granted access to the network with the specified public key.
Expand Down
Binary file modified contracts/sysio.roa/sysio.roa.wasm
Binary file not shown.
12 changes: 12 additions & 0 deletions contracts/sysio.uwrit/sysio.uwrit.abi
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,18 @@
{
"name": "ATTESTATION_TYPE_EMISSIONS_BLOCKED",
"value": 60962
},
{
"name": "ATTESTATION_TYPE_SYNDICATE_LIQ",
"value": 60963
},
{
"name": "ATTESTATION_TYPE_LIQ_YIELD",
"value": 60964
},
{
"name": "ATTESTATION_TYPE_DESYNDICATE_LIQ",
"value": 60965
}
]
},
Expand Down
Binary file modified contracts/test_contracts/sendinline/sendinline.wasm
Binary file not shown.
179 changes: 179 additions & 0 deletions contracts/tests/sysio.roa_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@
#include <fc/crypto/elliptic_em.hpp>
#include <fc/crypto/private_key.hpp>
#include <boost/test/unit_test.hpp>
#include <algorithm>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <type_traits>

using namespace sysio::testing;
Expand All @@ -31,6 +35,21 @@ using sysio::chain::compute_table_id;
constexpr account_name ROA = "sysio.roa"_n;
constexpr uint64_t NETWORK_GEN = 0;

/// Generated sub-account names newuser checks before giving up.
constexpr size_t NEWUSER_MAX_NAME_ATTEMPTS = 100;
/// newuser's abort when the nonce was already used by the same creator.
constexpr auto NEWUSER_DUPLICATE_NONCE = "Sponsor entry for this nonce already exists";
/// Every character newuser can place in a generated sub-account name: the name alphabet without '.'.
constexpr std::string_view GENERATED_NAME_CHARS = "12345abcdefghijklmnopqrstuvwxyz";
/// Longest account name supported by the contract.
constexpr size_t MAX_ACCOUNT_NAME_LENGTH = 12;

/// Returns newuser's error after its bounded generated-name search is exhausted.
static std::string newuser_names_exhausted_message() {
return "Failed to generate a unique account name after " +
std::to_string(NEWUSER_MAX_NAME_ATTEMPTS) + " attempts";
}

class sysio_roa_tester : public tester {
public:

Expand Down Expand Up @@ -257,6 +276,53 @@ class sysio_roa_tester : public tester {
return new_name;
}

/// Runs newuser with every account in `occupied` created first, committing nothing.
/// A second call with the same nonce aborts the transaction after the first call returns its
/// generated name, so every probe observes identical state in the same pending block.
transaction_trace_ptr push_newuser_probe( account_name creator, name nonce,
const vector<account_name>& occupied )
{
signed_transaction trx;
for (const auto& account : occupied) {
trx.actions.emplace_back( vector<permission_level>{{config::system_account_name,
config::active_name}},
newaccount{
.creator = config::system_account_name,
.name = account,
.owner = authority( get_public_key( account, "owner" ) ),
.active = authority( get_public_key( account, "active" ) ),
});
}
const auto call = get_action( ROA, "newuser"_n,
vector<permission_level>{{creator, config::sysio_payer_name},
{creator, config::active_name}},
mvo()("creator", creator)("nonce", nonce)
("pubkey", get_public_key(creator, "active")) );
trx.actions.push_back( call );
trx.actions.push_back( call );
set_transaction_headers( trx );
if (!occupied.empty())
trx.sign( get_private_key( config::system_account_name, "active" ), control->get_chain_id() );
trx.sign( get_private_key( creator, "active" ), control->get_chain_id() );
return push_transaction( trx, fc::time_point::maximum(), DEFAULT_BILLED_CPU_TIME_US,
true /* no_throw */ );
}

/// Returns the first newuser call's generated name from a rolled-back probe transaction.
static account_name newuser_probe_name( const transaction_trace& trace )
{
BOOST_REQUIRE( trace.except_ptr );
BOOST_REQUIRE_EXCEPTION( std::rethrow_exception( trace.except_ptr ),
sysio_assert_message_exception,
sysio_assert_message_is( NEWUSER_DUPLICATE_NONCE ) );
const auto it = std::find_if( trace.action_traces.begin(), trace.action_traces.end(),
[]( const auto& at ) {
return at.receiver == ROA && at.act.name == "newuser"_n;
});
BOOST_REQUIRE( it != trace.action_traces.end() );
return fc::raw::unpack<name>( it->return_value );
}

action_result regnodeowner( account_name owner, uint8_t tier )
{
return push_action(ROA, "forcereg"_n, mvo()
Expand Down Expand Up @@ -350,6 +416,119 @@ BOOST_FIXTURE_TEST_CASE( newuser_twice_test, sysio_roa_tester ) try {

} FC_LOG_AND_RETHROW()

/// Verifies newuser continues past the former three-attempt limit when generated names collide.
/// Each isolated tester starts from the same chain state and block number. Names returned by the
/// earlier testers are pre-created in the next tester, deterministically forcing one additional
/// collision without duplicating the contract's name-generation algorithm in test code.
BOOST_AUTO_TEST_CASE( newuser_retries_after_three_name_collisions ) try {
std::vector<account_name> occupied_names;
std::optional<uint32_t> newuser_block_num;

for (size_t collision_count = 0; collision_count <= 3; ++collision_count) {
auto chain = std::make_unique<sysio_roa_tester>();
BOOST_REQUIRE_EQUAL(chain->success(), chain->regnodeowner("alice"_n, 1));
chain->produce_blocks(1);

for (const auto occupied_name : occupied_names)
chain->create_account(occupied_name, config::system_account_name, false, false, false, false);

auto result = chain->newuser("alice"_n, "retrynonce"_n,
Comment thread
huangminghuang marked this conversation as resolved.
chain->get_public_key("alice"_n, "active"));
BOOST_REQUIRE(result && !result->action_traces.empty());

// A different block draws an unrelated sequence, so pin the generator input that makes the
// accounts collected from earlier fixtures collide in later fixtures.
if (!newuser_block_num)
newuser_block_num = result->block_num;
BOOST_REQUIRE_EQUAL(*newuser_block_num, result->block_num);

const auto generated_name = fc::raw::unpack<name>(result->action_traces[0].return_value);
BOOST_REQUIRE(std::find(occupied_names.begin(), occupied_names.end(), generated_name)
== occupied_names.end());
occupied_names.push_back(generated_name);
}

BOOST_REQUIRE_EQUAL(occupied_names.size(), 4u);
} FC_LOG_AND_RETHROW()

/// Verifies mixing the block number prevents related name nonces in different blocks from aliasing.
BOOST_AUTO_TEST_CASE( newuser_mixes_block_number_before_combining_nonce ) try {
// In Antelope name encoding, user1 ^ user2 equals this block delta shifted left 32 bits.
// The former linear seed therefore gave both calls the same candidate sequence.
constexpr uint32_t aliasing_block_distance = 384;
auto first = std::make_unique<sysio_roa_tester>();
auto second = std::make_unique<sysio_roa_tester>();
BOOST_REQUIRE_EQUAL(first->success(), first->regnodeowner("alice"_n, 1));
BOOST_REQUIRE_EQUAL(second->success(), second->regnodeowner("alice"_n, 1));
first->produce_blocks(1);
second->produce_blocks(1 + aliasing_block_distance);

const auto first_result = first->newuser("alice"_n, "user1"_n,
first->get_public_key("alice"_n, "active"));
const auto second_result = second->newuser("alice"_n, "user2"_n,
second->get_public_key("alice"_n, "active"));
BOOST_REQUIRE_EQUAL(first_result->block_num + aliasing_block_distance, second_result->block_num);

const auto first_name = fc::raw::unpack<name>(first_result->action_traces[0].return_value);
const auto second_name = fc::raw::unpack<name>(second_result->action_traces[0].return_value);
BOOST_REQUIRE_NE(first_name, second_name);
} FC_LOG_AND_RETHROW()

/// Pins newuser's generated-name search at exactly NEWUSER_MAX_NAME_ATTEMPTS attempts.
/// Probes roll back in one pending block. Occupying each returned candidate moves the next probe
/// one step farther through the deterministic sequence without duplicating the generator in test code.
BOOST_FIXTURE_TEST_CASE( newuser_tries_exactly_max_name_attempts, sysio_roa_tester ) try {
constexpr account_name creator = "alice"_n;
constexpr name nonce = "boundnonce"_n;
BOOST_REQUIRE_EQUAL(success(), regnodeowner(creator, 1));
produce_blocks(1);

std::vector<account_name> occupied;
std::optional<uint32_t> probe_block_num;
for (size_t attempt = 0; attempt < NEWUSER_MAX_NAME_ATTEMPTS; ++attempt) {
BOOST_TEST_CONTEXT("probe " << attempt) {
const auto trace = push_newuser_probe(creator, nonce, occupied);
if (!probe_block_num)
probe_block_num = trace->block_num;
BOOST_REQUIRE_EQUAL(*probe_block_num, trace->block_num);

const auto generated = newuser_probe_name(*trace);
BOOST_REQUIRE(std::find(occupied.begin(), occupied.end(), generated) == occupied.end());
occupied.push_back(generated);
}
}

const auto trace = push_newuser_probe(creator, nonce, occupied);
BOOST_REQUIRE_EQUAL(*probe_block_num, trace->block_num);
BOOST_REQUIRE(trace->except_ptr);
BOOST_REQUIRE_EXCEPTION(std::rethrow_exception(trace->except_ptr),
sysio_assert_message_exception,
sysio_assert_message_is(newuser_names_exhausted_message()));
BOOST_REQUIRE(get_sponsorship(creator, nonce).is_null());
BOOST_REQUIRE_EQUAL(0, get_sponsor_count(creator));
} FC_LOG_AND_RETHROW()

/// Verifies newuser records no sponsorship when the creator's entire generated namespace is occupied.
BOOST_FIXTURE_TEST_CASE( newuser_fails_when_generated_names_exhausted, sysio_roa_tester ) try {
constexpr account_name creator = "nodeowner1"_n;
constexpr name nonce = "fullspace"_n;
const std::string prefix = creator.to_string() + '.';
BOOST_REQUIRE_EQUAL(prefix.size() + 1, MAX_ACCOUNT_NAME_LENGTH);

create_account(creator, config::system_account_name, false, false, false, false);
BOOST_REQUIRE_EQUAL(success(), regnodeowner(creator, 1));

for (const char c : GENERATED_NAME_CHARS)
create_account(name(prefix + c), config::system_account_name, false, false, false, false);
produce_blocks(1);

BOOST_REQUIRE_EXCEPTION(newuser(creator, nonce, get_public_key(creator, "active")),
sysio_assert_message_exception,
sysio_assert_message_is(newuser_names_exhausted_message()));
BOOST_REQUIRE(get_sponsorship(creator, nonce).is_null());
BOOST_REQUIRE_EQUAL(0, get_sponsor_count(creator));
} FC_LOG_AND_RETHROW()

BOOST_FIXTURE_TEST_CASE( newuser_creator_permission_test, sysio_roa_tester ) try {
auto result = regnodeowner("alice"_n, 2);
BOOST_REQUIRE_EQUAL(success(), result);
Expand Down
Loading