-
Notifications
You must be signed in to change notification settings - Fork 110
swap, uint256: unify variable types, pt. 1 #2063
Conversation
…AvailableBalance to use Uint256
…alues, small refactor to Uint256 functions
swap/types.go
Outdated
// Sub sets u to minuend - subtrahend and returns u as the difference | ||
// returns an error when the result falls outside of the unsigned 256-bit integer range | ||
func (u *Uint256) Sub(minuend, subtrahend *Uint256) (*Uint256, error) { | ||
difference := new(big.Int).Sub(minuend.Value, subtrahend.Value) |
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 create new big.Int
s on every arithmetic operation anyway is there an advantage to having this "set receiver to result" api style? I believe the reason for this design is to avoid allocations happening during normal arithmetic operations but since a new big.Int
is allocated here anyway could we not just allocate an entirely new Uint256
and have a more "readable" API like in many other big number implementations (where big nums are immutable):
x = NewUint256(bigIntValue)
z = x.Add(y) // x remains unmodified
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 you point out, for something like z := x.Add(y)
to work (if we want x
to remain unmodified) we'd need to call NewUint256
inside of Add
, which in my opinion is less compliant with SRP than the current version.
i do see how x, err = x.Add(y)
can look better than _, err = x.Add(x,y)
. although, in this case of doing x += y
, x
would have its value replaced.
for the record: the "set receiver to result" style was modelled after the math/big
Int
functions, which was the basis for this design (in the spirit of not re-inventing the wheel).
i don't find it particularly hard to read, but in principle i'm not opposed to changing it.
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.
Before doing any further review, I would like to hear others' opinion about this approach.
While the idea is good, I think there are some implications which I am not sure I agree to or are even correct.
An alternative path could be inspired by https://ethereum.stackexchange.com/a/60699
swap/types.go
Outdated
value big.Int | ||
} | ||
|
||
var minUint256 = big.NewInt(0) |
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 had this in a different PR, I think these should be constants, not variables
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 far as i know, const
declarations in general cannot contain function calls, as they must have concrete values at compile time.
we need several function calls to obtain the 2^256-1
number, which cannot even be expressed as an integer literal in go code.
so how could we have these as constants?
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.
By putting the literal (calculated value) for example?
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 too big for a go literal, it won't compile
swap/types.go
Outdated
|
||
// Uint256 represents an unsigned integer of 256 bits | ||
type Uint256 struct { | ||
value big.Int |
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.
To be discussed: maybe it is worth considering using a fixed size byte array instead of this
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 not a byte array, we should probably return to using a pointer and not the value, otherwise we get unintended consequences
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.
the pointer was removed in the first place to avoid nil
values in Uint256
variables. this way, there's always at least an underlying value of 0
.
swap/types.go
Outdated
// NewUint256 creates a Uint256 struct with a minimum initial underlying value | ||
func NewUint256() *Uint256 { | ||
u := new(Uint256) | ||
u.value = *minUint256 |
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.
This actually sets value
to the actual Int
instance, check how Int
is defined. This is not doing what it looks it is doing....
fmt.Println(u.value)
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 sorry, i'm not clear on what you mean here.
can you explain what the correct code would be? i'm not seeing the problem with the output of this fmt.Println
call.
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.
apologies: if you meant the problem pointed out below in the Set
function, it should also be fixed now.
thanks
swap/types.go
Outdated
|
||
// Set assigns the underlying value of the given Uint256 param to u, and returns the modified receiver struct | ||
// returns an error when the result falls outside of the unsigned 256-bit integer range | ||
func (u *Uint256) Set(value big.Int) (*Uint256, error) { |
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.
Consider this:
v := NewUint256()
val := big.NewInt(5)
v.Set(*val) // set v to 5
val.SetInt64(9999) // now v AND val have value 9999
Is this intended?
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.
thanks, this was indeed a classic pointer bug.
should be fixed in the latest commit
swap/types.go
Outdated
} | ||
|
||
// Copy sets the underlying value of u to a copy of the given Uint256 param, and returns the modified receiver struct | ||
func (u *Uint256) Copy(v *Uint256) *Uint256 { |
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 is not clear what you want to achieve here; if it is a deep copy, then that is not what is happening, but basically the same as noted in Set
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.
this should be fixed with the latest commit, but to clarify: the intention is to have a sort of constructor function based on an existing *Uint256
var—if these are created correctly (and they were not, as you pointed out) we can safely "clone" them. that's the idea
swap/types.go
Outdated
var maxUint256 = new(big.Int).Sub(new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), big.NewInt(1)) // 2^256 - 1 | ||
|
||
// NewUint256 creates a Uint256 struct with a minimum initial underlying value | ||
func NewUint256() *Uint256 { |
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 would expect to be able to set the new value right in the constructor, like big.NewInt(val)
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.
of course. i will explain my reasoning here: NewUint256
returns only a Uint256
pointer, which is a comfortable condition to have. the same thing happens with the Uint64ToUint256
function.
if we receive a big.Int
param, we need to output an err
variable as well, since it's possible that the received param falls outside of the allowed range (as it happens in Set
).
this would not allow us to do calls like:
newCumulativePayout, err := NewUint256().Add(cumulativePayout, castedPrice)
because the constructor now has 2 variables as output.
however, I am not opposed to adding a new constructor with this behaviour. so instead of:
NewUint256().Set(*tentativeLiquidBalance)
we'd have something like:
NewUint256FromBigInt(*tentativeLiquidBalance)
although my choice for the NewUint256FromBigInt
name is pretty bad.
(name suggestions/changes are welcome)
swap/types.go
Outdated
return u.value.Cmp(&v.value) | ||
} | ||
|
||
// Equals returns true if the two Uint256 structs have the same underlying values, false otherwise |
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 is not needed, as Cmp
does the job
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.
for me, it's more readable to have this:
if !actualAmount.Equals(increase) {
...
}
than this:
if actualAmount.Cmp(increase) == 0 {
...
}
swap/types.go
Outdated
|
||
// Add sets u to augend + addend and returns u as the sum | ||
// returns an error when the result falls outside of the unsigned 256-bit integer range | ||
func (u *Uint256) Add(augend, addend *Uint256) (*Uint256, error) { |
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 big.Int
actually be embedded rather than a field, then we could use directly the big.Int
methods for operations
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, what do you mean by this? we are already calling big.Int
's Add
here
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.
nevermind, i see what you mean: you're saying directly through the receiver variable; something like:
type Uint256 struct {
big.Int
}
no?
but how could we do the over/underflow check then?
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.
Methods woulds still be on the actual type. But never mind, at this stage not needed to explore further.
…oUint256 and Set functions
we did run into this one a couple of times in the past. i think it could be part of the solution if we were to implement this through arrays, but we'd still need to implement all the arithmetic functions, and the conversion in the opposite direction, at least. |
…-types # Conflicts: # swap/swap.go # swap/swap_test.go
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.
Would you consider creating a package for Uint256 to reduce the size of the swap package?
swap/types.go
Outdated
@@ -49,3 +54,120 @@ type EmitChequeMsg struct { | |||
type ConfirmChequeMsg struct { | |||
Cheque *Cheque | |||
} | |||
|
|||
// Uint256 represents an unsigned integer of 256 bits |
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 looks to me that Uint256 implementation could be in its own file, or even package as it is already exposed.
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.
thanks for the suggestion, this should be implemented through #2076
…ge (#2076) swap, uint256: move Uint256 implementation to new swarm/uint256 package
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. Thanks @mortelli.
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.
A couple of minor things and a more important question:
Should we now not store the balance
and use setBalance
with Uint256 too?
} | ||
|
||
return actualAmount, nil | ||
} | ||
|
||
func (cheque *Cheque) String() string { | ||
return fmt.Sprintf("Contract: %x Beneficiary: %x CumulativePayout: %d Honey: %d", cheque.Contract, cheque.Beneficiary, cheque.CumulativePayout, cheque.Honey) | ||
return fmt.Sprintf("Contract: %x Beneficiary: %x CumulativePayout: %v Honey: %d", cheque.Contract, cheque.Beneficiary, cheque.CumulativePayout, cheque.Honey) |
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'd probably print %s, cheque.CumulativePayout.String()
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 shouldn't need to call String()
, right? Uint256
implements the Stringer
interface, ergo it knows how to represent itself as a string without calling the function explicitly, no?
now: do we use %v
or %s
? as far as i know there is no difference (in practice) as long as the interface is implemented, so i have no preference here.
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 it is a String I'd use %s
, which is what I actually meant in the first place
@@ -1129,12 +1144,15 @@ func TestPeerProcessAndVerifyChequeInvalid(t *testing.T) { | |||
} | |||
|
|||
if peer.getLastReceivedCheque().CumulativePayout != cheque.CumulativePayout { | |||
t.Fatalf("last received cheque has wrong cumulative payout, was: %d, expected: %d", peer.lastReceivedCheque.CumulativePayout, cheque.CumulativePayout) | |||
t.Fatalf("last received cheque has wrong cumulative payout, was: %v, expected: %v", peer.lastReceivedCheque.CumulativePayout, cheque.CumulativePayout) |
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'd print the String
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.
see above
@@ -1242,8 +1260,8 @@ func TestPeerGetLastSentCumulativePayout(t *testing.T) { | |||
_, peer, clean := newTestSwapAndPeer(t, ownerKey) | |||
defer clean() | |||
|
|||
if peer.getLastSentCumulativePayout() != 0 { | |||
t.Fatalf("last cumulative payout should be 0 in the beginning, was %d", peer.getLastSentCumulativePayout()) | |||
if !peer.getLastSentCumulativePayout().Equals(uint256.FromUint64(0)) { |
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.
Wouldn't it be better to compare just to the New()
value instead to run FromUint64
?
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 are reading this line by itself, how do you know that:
New()
will result in a value which can safely be compared throughEquals
?- assuming
New()
results in a safe value, that it is the minimum valid value? (and maybe even: that the minimum valid value is0
)
i will concede that this is a verbose mess considering we just want to say zero
. however: i think it's clearer this way.
i am happy to discuss it further though
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 am pointing this out only because I have seen elsewhere you have used just that New()
for a zero value, for example for the return value for getLastSentCumulativePayout
yes! we should. i envisioned this as pt. 2, actually. i didn't include this change because the branch in its current state is meaty enough for pt. 1—specially considering the discussion that needed to be had given that it was the first part of the solution. |
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.
Really good job!
a note, since i am about to begin pt. 2: balances can actually be negative, so we should use a different type. i might continue with a different field, but we'll get to that one eventually. |
This PR is the first step in implementing issue #1581, which aims at unifying variable types between the SWAP smart contracts and the related code on the go side.
Changes:
Uint256
type. This struct is merely a wrapper around abig.Int
field, but it operates through functions that change its underlying value while controlling over and underflows.Uint64ToUint256
function to be retired once the issue is fully implemented. We should eventually only convert to and frombig.Int
(which is required by the contract bindings) andUint256
(or corresponding) types. This should be achieved once all field types are unified.CumulativePayout
ChequeParams
field from theuint64
type to the*Uint256
type.AvailableBalance
,encodeForSignature
,verifyChequeAgainstLast
, (Cheque
's)String
,getLastSentCumulativePayout
,createCheque
,handleEmitChequeMsg
,cashCheque
andprocessAndVerifyCheque
functions has been changed as well.