Skip to content
This repository has been archived by the owner on Aug 2, 2021. It is now read-only.

swap, uint256: unify variable types, pt. 1 #2063

Merged
merged 67 commits into from
Jan 15, 2020
Merged

Conversation

mortelli
Copy link
Contributor

@mortelli mortelli commented Dec 24, 2019

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:

  • Adds the Uint256 type. This struct is merely a wrapper around a big.Int field, but it operates through functions that change its underlying value while controlling over and underflows.
    • I expect the Uint64ToUint256 function to be retired once the issue is fully implemented. We should eventually only convert to and from big.Int (which is required by the contract bindings) and Uint256 (or corresponding) types. This should be achieved once all field types are unified.
  • Changes the CumulativePayout ChequeParams field from the uint64 type to the *Uint256 type.
    • as a result, the code and/or signature for the AvailableBalance, encodeForSignature, verifyChequeAgainstLast, (Cheque's) String, getLastSentCumulativePayout, createCheque, handleEmitChequeMsg, cashCheque and processAndVerifyCheque functions has been changed as well.
  • Update tests to conform with these changes.

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)
Copy link
Member

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.Ints 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

Copy link
Contributor Author

@mortelli mortelli Jan 8, 2020

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.

Copy link
Contributor

@holisticode holisticode left a 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)
Copy link
Contributor

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

Copy link
Contributor Author

@mortelli mortelli Jan 8, 2020

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?

Copy link
Contributor

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?

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 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
Copy link
Contributor

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

Copy link
Contributor

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

Copy link
Contributor Author

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
Copy link
Contributor

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)

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'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.

Copy link
Contributor Author

@mortelli mortelli Jan 8, 2020

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) {
Copy link
Contributor

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?

Copy link
Contributor Author

@mortelli mortelli Jan 8, 2020

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 {
Copy link
Contributor

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

Copy link
Contributor Author

@mortelli mortelli Jan 8, 2020

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 {
Copy link
Contributor

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)

Copy link
Contributor Author

@mortelli mortelli Jan 8, 2020

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
Copy link
Contributor

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

Copy link
Contributor Author

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) {
Copy link
Contributor

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

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, what do you mean by this? we are already calling big.Int's Add here

Copy link
Contributor Author

@mortelli mortelli Jan 8, 2020

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?

Copy link
Contributor

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.

@mortelli
Copy link
Contributor Author

mortelli commented Jan 8, 2020

An alternative path could be inspired by https://ethereum.stackexchange.com/a/60699

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.

Copy link
Member

@janos janos left a 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
Copy link
Member

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.

Copy link
Contributor Author

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
@mortelli mortelli changed the title swap: unify variable types, pt. 1 swap, uint256: unify variable types, pt. 1 Jan 15, 2020
Copy link
Member

@janos janos left a comment

Choose a reason for hiding this comment

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

LGTM. Thanks @mortelli.

uint256/uint256.go Outdated Show resolved Hide resolved
Copy link
Contributor

@holisticode holisticode left a 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?

swap/cheque.go Show resolved Hide resolved
}

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)
Copy link
Contributor

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()

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 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.

Copy link
Contributor

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

swap/peer.go Show resolved Hide resolved
@@ -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)
Copy link
Contributor

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

Copy link
Contributor Author

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)) {
Copy link
Contributor

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?

Copy link
Contributor Author

@mortelli mortelli Jan 15, 2020

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 through Equals?
  • assuming New() results in a safe value, that it is the minimum valid value? (and maybe even: that the minimum valid value is 0)

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

Copy link
Contributor

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

@mortelli
Copy link
Contributor Author

A couple of minor things and a more important question:

Should we now not store the balance and use setBalance with Uint256 too?

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.

Copy link
Contributor

@holisticode holisticode left a comment

Choose a reason for hiding this comment

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

Really good job!

@mortelli mortelli merged commit 0bc6bef into master Jan 15, 2020
@acud acud added this to the 0.5.5 milestone Jan 21, 2020
@mortelli
Copy link
Contributor Author

mortelli commented Jan 22, 2020

A couple of minor things and a more important question:
Should we now not store the balance and use setBalance with Uint256 too?

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.

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.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants