If we were to introduce a third tier, a similar method would have to be found to reduce the weight of the new transaction type even further, which would be challenging if we are limited to a soft-fork scenario. It would be perhaps easier to introduce an intermediate tier between Segwit and legacy, for example limiting the current Segwit discount to simple script types. (Allowing a hard fork however, everything would be possible).
Changing fee structure alone, and nothing else, require no forks at all. You can create a new mining pool, and include your own rules. You can build 20-tier fee structure, and your blocks will be accepted. It is all about including transactions into your blocks, a simple binary decision: include or exclude. You can introduce any weird rules you can think about, and require no fees or 10x fees for vanity addresses containing "vjudeu". There are endless possibilities, and for example we already have almost 4 MB Ordinals transaction without any fees, and there was no fork needed to include it.
Can you elaborate or provide me a link?
You want to get a link to some
old client version?
Were there transactions which were completely free?
Of course. You can check some early blocks, and find many free transactions in the chain, for example check block 170, it has one non-coinbase transaction, and there is no fee. Even in the first released version, there was GetMinFee function:
int64 GetMinFee(bool fDiscount=false) const
{
unsigned int nBytes = ::GetSerializeSize(*this, SER_NETWORK);
if (fDiscount && nBytes < 10000)
return 0;
return (1 + (int64)nBytes / 1000) * CENT;
}
As you can see, there was at least two-tier fee structure. You can also see usage of this function:
// Transaction fee requirements, mainly only needed for flood control
// Under 10K (about 80 inputs) is free for first 100 transactions
// Base rate is 0.01 per KB
int64 nMinFee = tx.GetMinFee(pblock->vtx.size() < 100);
And another place, where it was called:
// Check that enough fee is included
if (nFee < wtxNew.GetMinFee(true))
{
nFee = nFeeRequiredRet = wtxNew.GetMinFee(true);
continue;
}
This model is older than priority-based rules, but by checking different versions, and grepping "GetMinFee" in the source code, you can see how it was changed over time. Today, you can find this code:
CFeeRate CTxMemPool::GetMinFee(size_t sizelimit) const {
LOCK(cs);
if (!blockSinceLastRollingFeeBump || rollingMinimumFeeRate == 0)
return CFeeRate(llround(rollingMinimumFeeRate));
int64_t time = GetTime();
if (time > lastRollingFeeUpdate + 10) {
double halflife = ROLLING_FEE_HALFLIFE;
if (DynamicMemoryUsage() < sizelimit / 4)
halflife /= 4;
else if (DynamicMemoryUsage() < sizelimit / 2)
halflife /= 2;
rollingMinimumFeeRate = rollingMinimumFeeRate / pow(2.0, (time - lastRollingFeeUpdate) / halflife);
lastRollingFeeUpdate = time;
if (rollingMinimumFeeRate < (double)m_incremental_relay_feerate.GetFeePerK() / 2) {
rollingMinimumFeeRate = 0;
return CFeeRate(0);
}
}
return std::max(CFeeRate(llround(rollingMinimumFeeRate)), m_incremental_relay_feerate);
}
You may notice that there is no hard-coded value like "thousand satoshis per virtual kilobyte", because you can change settings in your node, and even allow free transactions, without recompiling binaries. Of course, default settings are the most popular ones, and most pools enforce them, but no consensus changes are needed to change those rules.