-
Notifications
You must be signed in to change notification settings - Fork 14
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
Conversation
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 |
|
||
if (_allowance.gt(ArbitrationConstants.MAX_ALLOWANCE)) { | ||
_allowance = ArbitrationConstants.MAX_ALLOWANCE; | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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.
prt/contracts/src/tournament/factories/multilevel/TopTournamentFactory.sol
Outdated
Show resolved
Hide resolved
There was a problem hiding this 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"; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
||
if (_allowance.gt(ArbitrationConstants.MAX_ALLOWANCE)) { | ||
_allowance = ArbitrationConstants.MAX_ALLOWANCE; | ||
} |
There was a problem hiding this comment.
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.
uint64[] memory _log2step, | ||
uint64[] memory _height |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 (uint64, uint64)[]
uses (uint64[],uint64[])
uses
Array of structs | Struct of arrays | |
---|---|---|
1 | 2 | 4 |
2 | 3 | 4 |
3 | 4 | 4 |
4 | 5 | 4 |
So, for
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)
.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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%) |
prt/contracts/src/tournament/factories/multilevel/TopTournamentFactory.sol
Outdated
Show resolved
Hide resolved
uint64 log2step, | ||
uint64 height |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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);
There was a problem hiding this comment.
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).
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. |
Even with structs? Which tests are breaking? |
Even on 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 = [ |
I think this is the issue: We're using |
Thanks for the research! I've opened PR #89 to address this. |
b7caf54
to
2a5256e
Compare
Resolved conflicts, but will try to adopt the second approach suggested by @GCdePaula. |
e540c77
to
6fe9ce1
Compare
6fe9ce1
to
9d209ee
Compare
There was a problem hiding this 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"; |
There was a problem hiding this comment.
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 .
ArbitrationConstants.MATCH_EFFORT, | ||
ArbitrationConstants.MAX_ALLOWANCE, | ||
log2step, | ||
height |
There was a problem hiding this comment.
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
}
struct TournamentParameters { | ||
uint64 levels; | ||
uint64 log2step; | ||
uint64 height; | ||
Time.Duration matchEffort; | ||
Time.Duration maxAllowance; | ||
} |
There was a problem hiding this comment.
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
andlog2step
become an indexing operation;levels
queries the size of the array;- etc.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
uint64[] memory _log2step, | ||
uint64[] memory _height |
There was a problem hiding this comment.
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.
c330b5b
to
d3ae52f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
d3ae52f
to
c4619a6
Compare
Sorry, forgot a constant there. 😅 |
Parameterizes the following constants from
ArbitrationConstants
:MATCH_EFFORT
MAX_ALLOWANCE
LEVELS
log2step[]
height[]
Closes #85