Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parameterize constants #88

Merged
merged 1 commit into from
Dec 13, 2024

Conversation

guidanoli
Copy link
Contributor

@guidanoli guidanoli commented Dec 10, 2024

Parameterizes the following constants from ArbitrationConstants:

  • MATCH_EFFORT
  • MAX_ALLOWANCE
  • LEVELS
  • log2step[]
  • height[]

Closes #85

@guidanoli guidanoli marked this pull request as ready for review December 11, 2024 01:01
@guidanoli
Copy link
Contributor Author

I tried adding each parameter separately as constructor arguments but ended in the stack-too-deep problem. Turning Via-IR ON would break the tests for some reason. So, I decided to pass just the address of the factory contract so that it could be queried later for the parameters through the ITournamentParameters interface.

Comment on lines -96 to -107

if (_allowance.gt(ArbitrationConstants.MAX_ALLOWANCE)) {
_allowance = ArbitrationConstants.MAX_ALLOWANCE;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect this code block wasn't doing anything. So, I moved it before the assignment to allowance, and created a min function in the Time library to simplify code.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch. It's possible we can just remove it though. Let's discuss this more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a safeguard. If we can prove that _allowance will always be less than or equal to the max allowance, then we can remove this explicit cap.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My reasoning is that if we are sure the clock regeneration is capped, then the max of two capped things will also be capped.

We have to verify if the regeneration is capped.

) {
topFactory = _topFactory;
middleFactory = _middleFactory;
bottomFactory = _bottomFactory;

require(log2step.length == height.length, ArrayLengthMismatch());
require(log2step.length <= type(uint64).max, ArrayLengthTooLarge());
Copy link
Contributor Author

@guidanoli guidanoli Dec 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe an array this big is so unlikely that this line could be converted into an assertion.

Copy link
Collaborator

@GCdePaula GCdePaula left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that now tournaments receive both an IFactory and an ITournamentParameters. But aren't these the same address? Couldn't we pass it once?

Like, maybe we should make IFactory inherit from ITournamentParameters, so that we pass only one address (instead of the same address twice).

This conceptually makes sense, since a factory would also specify what are the tournament parameters. We should eventually also make it specify the step.

===

Another approach. First, let's keep the factory defining the parameters as described in the previous paragraph.

This specification is in the form of two structs: one (A) that tells you the number of levels and an array of pairs that tell the log2step and height, and another (B) that tells the time parameters. These are constant. Maybe we should create a third struct C that contains A * B.

The approach is as follows: instead of passing an address where these informations are queryable, could we pass down C?

===

So two approaches, different than what's on the PR.

The first approach is similar to the current one, but the parameters and the factory are unified. Like, they already are at the implementation level (I think), but not at the interface level. This forces you to pass two addresses (with the same value), when we could pass a single address.

The second approach passes down an immutable struct, instead of an interface that once queried returns these constants.

@@ -9,15 +9,28 @@ import {Machine} from "src/Machine.sol";

import "src/tournament/factories/MultiLevelTournamentFactory.sol";
import "src/IDataProvider.sol";
import "src/CanonicalConstants.sol";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this library has a different purpose now.

As such, I think the library name inside CanonicalConstants.sol should be called CanonicalConstants and should perhaps be inside the script/ directory.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just noticed we are still using CanonicalConstants under src/tournament, namely the constants LOG2_UARCH_SPAN and LOG2_INPUT_SPAN. Should those be parameterized as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I forgot a MAX_ALLOWANCE under src/tournament/libs/Clock.sol but I'm patching this right now.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, they should be part of meta-step. This is a "harder" kind of constant, which is really part of the code. It doesn't make a lot of sense to be parametrized.

I think for now we can hardcode them on the leaf tournament. In the future, when we extract the step to the factory, we should put them there. At some point we could make configuring it easier, but I don't think these are values that we need to change a lot.

New releases of the machine could change these parameters. I don't think making them configurable helps our tests .

@@ -140,7 +140,7 @@ abstract contract NonLeafTournament is Tournament {
// the inner tournament is bottom tournament at last level
// else instantiate middle tournament
Tournament _tournament;
if (_level == ArbitrationConstants.LEVELS - 1) {
if (_level == levels - 1) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this will break a lot of things, but...

Could it make sense for levels to decrement instead of increment? Maybe @stephenctw can help here.

The idea would be to have level 0 be the leaf tournament, and level LEVELS be the root. Then this check would be _level == 1, and we wouldn't need to pass down the LEVELS constant.

Don't know, just something that came to mind. I prefer not to break things, so maybe just ignore what I said.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes perfect sense to have a counter be decremented until a constant value (e.g., 0 or 1) instead of being incremented until a variable, because it allows us to remove such variable. However, I don't know the impact this would have on the PRT contracts, since a tournament to be a leaf and/or a root seem to be orthogonal properties, such that we have the four concrete combinations.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, also other annoying changes. We'd have to reverse the order in the array of height+log2step, and even worse all the cascading changes offchain.

Let's consider this again later.

Comment on lines -96 to -107

if (_allowance.gt(ArbitrationConstants.MAX_ALLOWANCE)) {
_allowance = ArbitrationConstants.MAX_ALLOWANCE;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch. It's possible we can just remove it though. Let's discuss this more.

Comment on lines 35 to 36
uint64[] memory _log2step,
uint64[] memory _height
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking if we should create a struct of log2step * height, and have a single array of that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could, but it would take up more storage slots. Solidity reserves one entire storage slot for each (uint64,uint64) tuple (while it could, in theory, pack two into the same storage slot). Meanwhile, it tightly packs four uint64 values into the same storage slot. So, storing log2step and height as two separate arrays uses asymptotically half as many storage slots.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not super familiar with Solidity packing. I'm very confused about all this. Here are a bunch of questions.

Does all of this matter if it's immutable? We're using dynamic arrays. Is there really packing of dynamic arrays in storage? Can we actually make dynamic arrays immutable?

I believe static arrays and structs in memory are padded to 32 bytes, but both are packed when in storage. Like, I don't know how much space (uint64, uint64)[] would take in storage when compared with (uint64[], uint64[]). Could the second be worse because we'd be storing the dynamic array's size twice?

My conclusions are:

  • I don't think it makes a difference in memory since it's not packed.
  • Even if it did, I'm not worried about memory since the size is 3.
  • In storage, since the array is dynamic, is it really tightly packed in either alternative?
  • It could even be worse to have two dynamic arrays because we'd be storing size twice.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there really packing of dynamic arrays in storage?

There is. For example, uint64[] arrays store four elements per slot.

Can we actually make dynamic arrays immutable?

As of now, Solidity doesn't support immutable dynamic arrays.

Could the second be worse because we'd be storing the dynamic array's size twice?

So, being $n$ the number of levels of tournament, (uint64, uint64)[] uses $\left( 1 + n \right)$ storage slots, whereas (uint64[],uint64[]) uses $\left[ 2 \cdot \left( 1 + \left\lceil \frac{n}{4} \right\rceil \right) \right]$ storage slots. Here are some numbers:

$n$ Array of structs Struct of arrays
1 2 4
2 3 4
3 4 4
4 5 4

So, for $n&lt;3$, the first option uses less storage slots. For $n=3$, both use the same number of storage slots. And for $n&gt;3$, the second option uses less storage slots.

In storage, since the array is dynamic, is it really tightly packed in either alternative?

Only if the array elements are of value types like uint64 but not tuples like (uint64,uint64).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great analysis! This cleared up my confusion.

I guess the conclusion is that gas-wise it's pretty much irrelevant, we can choose whatever we prefer since it won't impact costs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, gas-wise your solution is better because it needs to read only one array from storage when instantiating a tournament, instead of two. Also, we don't need to check if the array lengths match at construction time. Here is the gas diff:

Contract Function Min Avg Max
MiddleTournament sealInnerMatchAndCreateInnerTournament -3950 (-0.10%) -3950 (-0.10%) -3950 (-0.10%)
MultiLevelTournamentFactory -17324 (-2.97%)
TopTournament sealInnerMatchAndCreateInnerTournament -3950 (-0.22%) -3950 (-0.21%) -3950 (-0.21%)

Comment on lines +82 to +83
uint64 log2step,
uint64 height
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should maybe create a struct with this pair.

Copy link
Contributor Author

@guidanoli guidanoli Dec 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing so would increase the gas cost of the joinTournament function. Here is the diff:

diff --git a/prt/contracts/src/tournament/abstracts/Tournament.sol b/prt/contracts/src/tournament/abstracts/Tournament.sol
index 9d27147..15780d6 100644
--- a/prt/contracts/src/tournament/abstracts/Tournament.sol
+++ b/prt/contracts/src/tournament/abstracts/Tournament.sol
@@ -348,8 +348,10 @@ abstract contract Tournament {
                 _rootHash,
                 _leftNode,
                 _rightNode,
-                log2step,
-                height
+                Match.Constants({
+                    log2step: log2step,
+                    height: height
+                })
             );
 
             matches[_matchId] = _matchState;
diff --git a/prt/contracts/src/tournament/libs/Match.sol b/prt/contracts/src/tournament/libs/Match.sol
index 5236a96..b1c58ba 100644
--- a/prt/contracts/src/tournament/libs/Match.sol
+++ b/prt/contracts/src/tournament/libs/Match.sol
@@ -74,13 +74,20 @@ library Match {
         uint64 height; // constant
     }
 
+    //
+    // Constants
+    //
+    struct Constants {
+        uint64 log2step;
+        uint64 height;
+    }
+
     function createMatch(
         Tree.Node one,
         Tree.Node two,
         Tree.Node leftNodeOfTwo,
         Tree.Node rightNodeOfTwo,
-        uint64 log2step,
-        uint64 height
+        Constants memory constants
     ) internal pure returns (IdHash, State memory) {
         assert(two.verify(leftNodeOfTwo, rightNodeOfTwo));
 
@@ -91,9 +98,9 @@ library Match {
             leftNode: leftNodeOfTwo,
             rightNode: rightNodeOfTwo,
             runningLeafPosition: 0,
-            currentHeight: height,
-            log2step: log2step,
-            height: height
+            currentHeight: constants.height,
+            log2step: constants.log2step,
+            height: constants.height
         });
 
         return (matchId.hashFromId(), state);

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel strongly about either alternative. My 2c is that if the gas difference is small, we should go for what we prefer (I don't have a strong preference for either).

@guidanoli
Copy link
Contributor Author

The approach is as follows: instead of passing an address where these informations are queryable, could we pass down C?

IMO this would be preferable, but this currently leads to the stack-too-deep problem. With Via-IR compilation option turned ON, this would be solved, but currently leads to tests strangely failing.

@GCdePaula
Copy link
Collaborator

Even with structs? Which tests are breaking?

@guidanoli
Copy link
Contributor Author

Even with structs? Which tests are breaking?

Even on main. It breaks testRootWinner with message tournament check-in elapsed.
You can check it for yourself by applying this diff on main:

diff --git a/prt/contracts/foundry.toml b/prt/contracts/foundry.toml
index a7cdd81..1e25967 100644
--- a/prt/contracts/foundry.toml
+++ b/prt/contracts/foundry.toml
@@ -2,6 +2,7 @@
 src = "src"
 out = "out"
 libs = ["lib"]
+via_ir = true
 
 allow_paths = ['../../machine/step/']
 remappings = [

@GCdePaula
Copy link
Collaborator

I think this is the issue:

foundry-rs/foundry#7911

We're using vim.warp and block.timestamp. I think we have to update the tests to be correct with via-ir.

@guidanoli
Copy link
Contributor Author

guidanoli commented Dec 12, 2024

I think this is the issue

Thanks for the research! I've opened PR #89 to address this.
If it gets merged, I think we can go with the second approach you suggested.

@guidanoli guidanoli force-pushed the feature/parameterize-constants-second-attempt branch from b7caf54 to 2a5256e Compare December 12, 2024 14:50
@guidanoli
Copy link
Contributor Author

Resolved conflicts, but will try to adopt the second approach suggested by @GCdePaula.

@guidanoli guidanoli force-pushed the feature/parameterize-constants-second-attempt branch 3 times, most recently from e540c77 to 6fe9ce1 Compare December 12, 2024 18:44
@guidanoli guidanoli requested a review from GCdePaula December 12, 2024 18:44
@guidanoli guidanoli force-pushed the feature/parameterize-constants-second-attempt branch from 6fe9ce1 to 9d209ee Compare December 12, 2024 20:43
Copy link
Collaborator

@GCdePaula GCdePaula left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a few comments! This is looking great.

@@ -9,15 +9,28 @@ import {Machine} from "src/Machine.sol";

import "src/tournament/factories/MultiLevelTournamentFactory.sol";
import "src/IDataProvider.sol";
import "src/CanonicalConstants.sol";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, they should be part of meta-step. This is a "harder" kind of constant, which is really part of the code. It doesn't make a lot of sense to be parametrized.

I think for now we can hardcode them on the leaf tournament. In the future, when we extract the step to the factory, we should put them there. At some point we could make configuring it easier, but I don't think these are values that we need to change a lot.

New releases of the machine could change these parameters. I don't think making them configurable helps our tests .

Comment on lines 30 to 33
ArbitrationConstants.MATCH_EFFORT,
ArbitrationConstants.MAX_ALLOWANCE,
log2step,
height
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a personal style preference, think it's more readable as structs. Something like:

struct TimeConstants {
    Time.Duration matchEffort,
    Time.Duration maxAllowance
}

struct CommitmentStructure {
    uint64 log2step,
    uint64 height
}

struct DisputeParameters {
  TimeConstants timeConstants,
  CommitmentStructure[] commitmentsGranularity
}

Comment on lines +8 to +14
struct TournamentParameters {
uint64 levels;
uint64 log2step;
uint64 height;
Time.Duration matchEffort;
Time.Duration maxAllowance;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you like the idea of DisputeParameters, I believe that struct would supersede this one. So each level holds its own level plus the DisputeParameters, and can know everything it needs:

  • height and log2step become an indexing operation;
  • levels queries the size of the array;
  • etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I don't understand your comment.

Copy link
Collaborator

@GCdePaula GCdePaula Dec 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rereading it is a bit confusing 😅

In a previous comment, I suggested (as a style preference) this approach:

struct TimeConstants {
    Time.Duration matchEffort,
    Time.Duration maxAllowance
}

struct CommitmentStructure {
    uint64 log2step,
    uint64 height
}

struct DisputeParameters {
  TimeConstants timeConstants,
  CommitmentStructure[] commitmentsGranularity
}

The idea is that this single DisputeParameters would be passed down the tournaments, instead of several separate parameters. It's the same thing, but condensed in a single struct. In this proposal, each tournament would hold its own level and the DisputeParameters.

If you like this idea, then the proposed DisputeParameters plus level would supersede the above TournamentParameters, since the former contains the latter in the following way:

tournamentParameters.levels == disputeParameters.commitmentsGranularity.length
tournamentParameters.log2step == disputeParameters.commitmentsGranularity[level].log2step
tournamentParameters.height == disputeParameters.commitmentsGranularity[level].height
tournamentParameters.matchEffort == disputeParameters.timeConstants.matchEffort
tournamentParameters.maxAllowance == disputeParameters.timeConstants.maxAllowance

Extending this idea further, there are other places (i.e. Match) where we store the pair (log2step * height). We could instead use CommitmentStructure. That is, when instantiating a match, we could pass down disputeParameters.commitmentsGranularity[level], instead of "destructuring" it into two integers.

This is also true for the Clock, where we could pass disputeParameters.timeConstants, instead matchEffort and maxAllowance separately.

Let me reiterate, this is a style preference. I think it's ok currently.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we were to pass down this struct to the tournament factory, we would have to read this whole array of log2step-height tuples from storage (because Solidity disallows immutable dynamic arrays), which is costly. In reality, we only need to pass down a single log2step-height tuple.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha! Makes total sense.

@@ -140,7 +140,7 @@ abstract contract NonLeafTournament is Tournament {
// the inner tournament is bottom tournament at last level
// else instantiate middle tournament
Tournament _tournament;
if (_level == ArbitrationConstants.LEVELS - 1) {
if (_level == levels - 1) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, also other annoying changes. We'd have to reverse the order in the array of height+log2step, and even worse all the cascading changes offchain.

Let's consider this again later.

Comment on lines 35 to 36
uint64[] memory _log2step,
uint64[] memory _height
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not super familiar with Solidity packing. I'm very confused about all this. Here are a bunch of questions.

Does all of this matter if it's immutable? We're using dynamic arrays. Is there really packing of dynamic arrays in storage? Can we actually make dynamic arrays immutable?

I believe static arrays and structs in memory are padded to 32 bytes, but both are packed when in storage. Like, I don't know how much space (uint64, uint64)[] would take in storage when compared with (uint64[], uint64[]). Could the second be worse because we'd be storing the dynamic array's size twice?

My conclusions are:

  • I don't think it makes a difference in memory since it's not packed.
  • Even if it did, I'm not worried about memory since the size is 3.
  • In storage, since the array is dynamic, is it really tightly packed in either alternative?
  • It could even be worse to have two dynamic arrays because we'd be storing size twice.

@guidanoli guidanoli force-pushed the feature/parameterize-constants-second-attempt branch 4 times, most recently from c330b5b to d3ae52f Compare December 13, 2024 13:37
@guidanoli guidanoli requested a review from GCdePaula December 13, 2024 13:46
GCdePaula
GCdePaula previously approved these changes Dec 13, 2024
Copy link
Collaborator

@GCdePaula GCdePaula left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@guidanoli guidanoli force-pushed the feature/parameterize-constants-second-attempt branch from d3ae52f to c4619a6 Compare December 13, 2024 14:06
@guidanoli
Copy link
Contributor Author

Sorry, forgot a constant there. 😅

@guidanoli guidanoli requested a review from GCdePaula December 13, 2024 14:07
@guidanoli guidanoli merged commit 20a32e5 into main Dec 13, 2024
1 check passed
@guidanoli guidanoli deleted the feature/parameterize-constants-second-attempt branch December 13, 2024 15:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Custom arbitration parameters
2 participants