diff --git a/app/app.go b/app/app.go index 6c8a9ae..2d61a7a 100644 --- a/app/app.go +++ b/app/app.go @@ -91,7 +91,7 @@ func NewChildChain(logger log.Logger, db dbm.DB, traceStore io.Writer, options . ) app.Router(). - AddRoute("spend", utxo.NewSpendHandler(app.utxoMapper, app.nextPosition, types.ProtoUTXO)) + AddRoute("spend", utxo.NewSpendHandler(app.utxoMapper, app.nextPosition)) app.MountStoresIAVL(app.capKeyMainStore) app.MountStoresIAVL(app.capKeyMetadataStore) @@ -124,7 +124,7 @@ func (app *ChildChain) initChainer(ctx sdk.Context, req abci.RequestInitChain) a // load the accounts for _, gutxo := range genesisState.UTXOs { utxo := ToUTXO(gutxo) - app.utxoMapper.AddUTXO(ctx, utxo) + app.utxoMapper.ReceiveUTXO(ctx, utxo) } app.validatorAddress = ethcmn.HexToAddress(genesisState.Validator.Address) @@ -144,14 +144,8 @@ func (app *ChildChain) endBlocker(ctx sdk.Context, req abci.RequestEndBlock) abc Oindex: 0, DepositNum: 0, } - utxo := types.BaseUTXO{ - Address: app.validatorAddress, - InputAddresses: [2]ethcmn.Address{app.validatorAddress, ethcmn.Address{}}, - Amount: app.feeAmount, - Denom: types.Denom, - Position: position, - } - app.utxoMapper.AddUTXO(ctx, &utxo) + utxo := utxo.NewUTXO(app.validatorAddress.Bytes(), app.feeAmount, types.Denom, position) + app.utxoMapper.ReceiveUTXO(ctx, utxo) } // reset txIndex and fee @@ -188,7 +182,7 @@ func (app *ChildChain) nextPosition(ctx sdk.Context, secondary bool) utxo.Positi return types.NewPlasmaPosition(uint64(ctx.BlockHeight()), app.txIndex-1, 1, 0) } -// Unimplemented for now +// Fee Updater passed into antehandler func (app *ChildChain) feeUpdater(output []utxo.Output) sdk.Error { if len(output) != 1 || output[0].Denom != types.Denom { return utxo.ErrInvalidFee(2, "Fee must be paid in Eth") diff --git a/app/app_test.go b/app/app_test.go index a64e02e..c1236c5 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -4,6 +4,7 @@ import ( "encoding/binary" "fmt" "os" + "reflect" "testing" "crypto/ecdsa" @@ -19,6 +20,7 @@ import ( types "github.com/FourthState/plasma-mvp-sidechain/types" utils "github.com/FourthState/plasma-mvp-sidechain/utils" + "github.com/FourthState/plasma-mvp-sidechain/x/utxo" rlp "github.com/ethereum/go-ethereum/rlp" ) @@ -103,6 +105,15 @@ func CreateConfirmSig(hash []byte, privKey0, privKey1 *ecdsa.PrivateKey, two_inp return confirmSigs } +func getInputKeys(mapper utxo.Mapper, inputs ...Input) (res [][]byte) { + for _, in := range inputs { + if !reflect.DeepEqual(in.addr, common.Address{}) { + res = append(res, mapper.ConstructKey(in.addr.Bytes(), in.position)) + } + } + return res +} + // helper for constructing single or double input tx func GetTx(msg types.SpendMsg, privKeyA, privKeyB *ecdsa.PrivateKey, two_sigs bool) (tx types.BaseTx) { hash := ethcrypto.Keccak256(msg.GetSignBytes()) @@ -196,12 +207,13 @@ func TestSpendDeposit(t *testing.T) { // Retrieve UTXO from context position := types.NewPlasmaPosition(1, 0, 0, 0) - utxo := cc.utxoMapper.GetUTXO(ctx, addrB.Bytes(), position) + res := cc.utxoMapper.GetUTXO(ctx, addrB.Bytes(), position) - expected := types.NewBaseUTXO(addrB, [2]common.Address{addrA, common.Address{}}, 100, "", position) - expected.TxHash = tmhash.Sum(txBytes) + inputKey := cc.utxoMapper.ConstructKey(addrA.Bytes(), types.NewPlasmaPosition(0, 0, 0, 1)) + txHash := tmhash.Sum(txBytes) + expected := utxo.NewUTXOwithInputs(addrB.Bytes(), 100, "Ether", position, txHash, [][]byte{inputKey}) - require.Equal(t, expected, utxo, "UTXO did not get added to store correctly") + require.Equal(t, expected, res, "UTXO did not get added to store correctly") } func TestSpendTx(t *testing.T) { @@ -263,12 +275,12 @@ func TestSpendTx(t *testing.T) { // Retrieve UTXO from context position := types.NewPlasmaPosition(5, 0, 0, 0) - utxo := cc.utxoMapper.GetUTXO(ctx, addrA.Bytes(), position) + actual := cc.utxoMapper.GetUTXO(ctx, addrA.Bytes(), position) - expected := types.NewBaseUTXO(addrA, [2]common.Address{addrB, common.Address{}}, 100, "", position) - expected.TxHash = txHash + inputKey := cc.utxoMapper.ConstructKey(addrB.Bytes(), types.NewPlasmaPosition(1, 0, 0, 0)) + expected := utxo.NewUTXOwithInputs(addrA.Bytes(), 100, "Ether", position, txHash, [][]byte{inputKey}) - require.Equal(t, expected, utxo, "UTXO did not get added to store correctly") + require.Equal(t, expected, actual, "UTXO did not get added to store correctly") } @@ -383,12 +395,10 @@ func TestDifferentTxForms(t *testing.T) { // note: all cases currently have inputs belonging to the previous tx // and therefore we only need to grab the first txhash from the inptus input_utxo := cc.utxoMapper.GetUTXO(ctx, tc.input0.addr.Bytes(), tc.input0.position) - baseutxo, _ := input_utxo.(*types.BaseUTXO) - txhash := baseutxo.TxHash blknumKey := make([]byte, binary.MaxVarintLen64) binary.PutUvarint(blknumKey, uint64(7+uint64(index-1))) blockhash := cc.metadataMapper.GetMetadata(ctx, blknumKey) - hash := tmhash.Sum(append(txhash, blockhash...)) + hash := tmhash.Sum(append(input_utxo.TxHash, blockhash...)) msg.Input0ConfirmSigs = CreateConfirmSig(hash, keys[tc.input0.input_index0], keys[input0_index1], tc.input0.input_index1 != -1) msg.Input1ConfirmSigs = CreateConfirmSig(hash, keys[input1_index0], keys[input1_index1], tc.input1.input_index1 != -1) @@ -403,30 +413,30 @@ func TestDifferentTxForms(t *testing.T) { // Retrieve utxo from context position := types.NewPlasmaPosition(uint64(index)+7, 0, 0, 0) - utxo := cc.utxoMapper.GetUTXO(ctx, tc.newowner0.Bytes(), position) + utxo0 := cc.utxoMapper.GetUTXO(ctx, tc.newowner0.Bytes(), position) - expected := types.NewBaseUTXO(tc.newowner0, [2]common.Address{msg.Owner0, msg.Owner1}, tc.amount0, "", position) - expected.TxHash = tmhash.Sum(txBytes) + inputKeys := getInputKeys(cc.utxoMapper, tc.input0, tc.input1) + txHash := tmhash.Sum(txBytes) + expected := utxo.NewUTXOwithInputs(tc.newowner0.Bytes(), tc.amount0, "Ether", position, txHash, inputKeys) - require.Equal(t, expected, utxo, fmt.Sprintf("First UTXO did not get added to the utxo store correctly. Failed on test case: %d", index)) + require.Equal(t, expected, utxo0, fmt.Sprintf("First UTXO did not get added to the utxo store correctly. Failed on test case: %d", index)) if !utils.ZeroAddress(msg.Newowner1) { position = types.NewPlasmaPosition(uint64(index)+7, 0, 1, 0) - utxo = cc.utxoMapper.GetUTXO(ctx, tc.newowner1.Bytes(), position) + utxo1 := cc.utxoMapper.GetUTXO(ctx, tc.newowner1.Bytes(), position) - expected = types.NewBaseUTXO(tc.newowner1, [2]common.Address{msg.Owner0, msg.Owner1}, tc.amount1, "", position) - expected.TxHash = tmhash.Sum(txBytes) + expected = utxo.NewUTXOwithInputs(tc.newowner1.Bytes(), tc.amount1, "Ether", position, txHash, inputKeys) - require.Equal(t, expected, utxo, fmt.Sprintf("Second UTXO did not get added to the utxo store correctly. Failed on test case: %d", index)) + require.Equal(t, expected, utxo1, fmt.Sprintf("Second UTXO did not get added to the utxo store correctly. Failed on test case: %d", index)) } // Check that inputs were removed - utxo = cc.utxoMapper.GetUTXO(ctx, msg.Owner0.Bytes(), tc.input0.position) - require.Nil(t, utxo, fmt.Sprintf("first input was not removed from the utxo store. Failed on test case: %d", index)) + recovered := cc.utxoMapper.GetUTXO(ctx, msg.Owner0.Bytes(), tc.input0.position) + require.False(t, recovered.Valid, fmt.Sprintf("first input was not removed from the utxo store. Failed on test case: %d", index)) if !utils.ZeroAddress(msg.Owner1) { - utxo = cc.utxoMapper.GetUTXO(ctx, msg.Owner1.Bytes(), tc.input1.position) - require.Nil(t, utxo, fmt.Sprintf("second input was not removed from the utxo store. Failed on test case: %d", index)) + recovered = cc.utxoMapper.GetUTXO(ctx, msg.Owner1.Bytes(), tc.input1.position) + require.False(t, recovered.Valid, fmt.Sprintf("second input was not removed from the utxo store. Failed on test case: %d", index)) } cc.EndBlock(abci.RequestEndBlock{}) @@ -469,16 +479,17 @@ func TestMultiTxBlocks(t *testing.T) { for i := uint16(0); i < N; i++ { txBytes, _ := rlp.EncodeToBytes(txs[i]) position := types.NewPlasmaPosition(1, i, 0, 0) - utxo := cc.utxoMapper.GetUTXO(ctx, addrs[i].Bytes(), position) + actual := cc.utxoMapper.GetUTXO(ctx, addrs[i].Bytes(), position) - expected := types.NewBaseUTXO(addrs[i], [2]common.Address{addrs[i], common.Address{}}, 100, "", position) + inputKey := cc.utxoMapper.ConstructKey(addrs[i].Bytes(), types.NewPlasmaPosition(0, 0, 0, uint64(i+1))) + expected := utxo.NewUTXOwithInputs(addrs[i].Bytes(), 100, "Ether", position, tmhash.Sum(txBytes), [][]byte{inputKey}) expected.TxHash = tmhash.Sum(txBytes) - require.Equal(t, expected, utxo, fmt.Sprintf("UTXO %d did not get added to store correctly", i+1)) + require.Equal(t, expected, actual, fmt.Sprintf("UTXO %d did not get added to store correctly", i+1)) position = types.NewPlasmaPosition(0, 0, 0, uint64(i)+1) - utxo = cc.utxoMapper.GetUTXO(ctx, addrs[i].Bytes(), position) - require.Nil(t, utxo, fmt.Sprintf("deposit %d did not get removed correctly from the utxo store", i+1)) + deposit := cc.utxoMapper.GetUTXO(ctx, addrs[i].Bytes(), position) + require.False(t, deposit.Valid, fmt.Sprintf("deposit %d did not get removed correctly from the utxo store", i+1)) } cc.EndBlock(abci.RequestEndBlock{}) @@ -514,15 +525,15 @@ func TestMultiTxBlocks(t *testing.T) { // Retrieve and check UTXO from context for i := uint16(0); i < N; i++ { txBytes, _ := rlp.EncodeToBytes(txs[i]) - utxo := cc.utxoMapper.GetUTXO(ctx, addrs[(i+1)%N].Bytes(), types.NewPlasmaPosition(2, i, 0, 0)) + actual := cc.utxoMapper.GetUTXO(ctx, addrs[(i+1)%N].Bytes(), types.NewPlasmaPosition(2, i, 0, 0)) - expected := types.NewBaseUTXO(addrs[(i+1)%N], [2]common.Address{addrs[i], common.Address{}}, 100, "", types.NewPlasmaPosition(2, i, 0, 0)) - expected.TxHash = tmhash.Sum(txBytes) + inputKey := cc.utxoMapper.ConstructKey(addrs[i].Bytes(), types.NewPlasmaPosition(1, i, 0, 0)) + expected := utxo.NewUTXOwithInputs(addrs[(i+1)%N].Bytes(), 100, "Ether", types.NewPlasmaPosition(2, i, 0, 0), tmhash.Sum(txBytes), [][]byte{inputKey}) - require.Equal(t, expected, utxo, fmt.Sprintf("UTXO %d did not get added to store correctly", i+1)) + require.Equal(t, expected, actual, fmt.Sprintf("UTXO %d did not get added to store correctly", i+1)) - utxo = cc.utxoMapper.GetUTXO(ctx, addrs[i].Bytes(), types.NewPlasmaPosition(1, i, 0, 0)) - require.Nil(t, utxo, fmt.Sprintf("UTXO %d did not get removed from the utxo store correctly", i)) + input := cc.utxoMapper.GetUTXO(ctx, addrs[i].Bytes(), types.NewPlasmaPosition(1, i, 0, 0)) + require.False(t, input.Valid, fmt.Sprintf("UTXO %d did not get removed from the utxo store correctly", i)) } } @@ -584,9 +595,9 @@ func TestFee(t *testing.T) { valUTXO := cc.utxoMapper.GetUTXO(ctx, valAddr.Bytes(), expectedValPosition) // Check that users and validators have expected UTXO's - require.Equal(t, uint64(90), utxo1.GetAmount(), "UTXO1 does not have expected amount") - require.Equal(t, uint64(90), utxo2.GetAmount(), "UTXO2 does not have expected amount") - require.Equal(t, uint64(20), valUTXO.GetAmount(), "Validator fees did not get collected into UTXO correctly") + require.Equal(t, uint64(90), utxo1.Amount, "UTXO1 does not have expected amount") + require.Equal(t, uint64(90), utxo2.Amount, "UTXO2 does not have expected amount") + require.Equal(t, uint64(20), valUTXO.Amount, "Validator fees did not get collected into UTXO correctly") // Check that validator can spend his fees as if they were a regular UTXO on sidechain valMsg := GenerateSimpleMsg(valAddr, addrs[0], [4]uint64{1, 1<<16 - 1, 0, 0}, 10, 10) @@ -606,5 +617,5 @@ func TestFee(t *testing.T) { // Check that fee Amount gets reset between blocks. feeAmount for block 2 is 10 not 30. feeUTXO2 := cc.utxoMapper.GetUTXO(ctx, valAddr.Bytes(), types.NewPlasmaPosition(2, 1<<16-1, 0, 0)) - require.Equal(t, uint64(10), feeUTXO2.GetAmount(), "Fee Amount on second block is incorrect") + require.Equal(t, uint64(10), feeUTXO2.Amount, "Fee Amount on second block is incorrect") } diff --git a/app/genesis.go b/app/genesis.go index b5784d8..623e152 100644 --- a/app/genesis.go +++ b/app/genesis.go @@ -49,20 +49,15 @@ func ToUTXO(gutxo GenesisUTXO) utxo.UTXO { // Any failed str conversion defaults to 0 addr := common.HexToAddress(gutxo.Address) amount, _ := strconv.ParseUint(gutxo.Denom, 10, 64) - utxo := &types.BaseUTXO{ - InputAddresses: [2]common.Address{addr, common.Address{}}, - Address: addr, - Amount: amount, - Denom: types.Denom, - } + blkNum, _ := strconv.ParseUint(gutxo.Position[0], 10, 64) txIndex, _ := strconv.ParseUint(gutxo.Position[1], 10, 16) oIndex, _ := strconv.ParseUint(gutxo.Position[2], 10, 8) depNum, _ := strconv.ParseUint(gutxo.Position[3], 10, 64) position := types.NewPlasmaPosition(blkNum, uint16(txIndex), uint8(oIndex), depNum) - utxo.SetPosition(position) - return utxo + + return utxo.NewUTXO(addr.Bytes(), amount, "Ether", position) } var ( diff --git a/auth/ante.go b/auth/ante.go index 3c0129c..ced59b7 100644 --- a/auth/ante.go +++ b/auth/ante.go @@ -125,36 +125,30 @@ func processConfirmSig( res sdk.Result) { // Verify utxo exists - utxo := utxoMapper.GetUTXO(ctx, addr.Bytes(), &position) - if utxo == nil { + input := utxoMapper.GetUTXO(ctx, addr.Bytes(), &position) + if reflect.DeepEqual(input, utxo.UTXO{}) { return sdk.ErrUnknownRequest(fmt.Sprintf("confirm Sig verification failed: UTXO trying to be spent, does not exist: %v.", position)).Result() } - plasmaUTXO, ok := utxo.(*types.BaseUTXO) - if !ok { - return sdk.ErrInternal("utxo must be of type BaseUTXO").Result() + // Get input addresses for input UTXO (grandfather inputs) + inputAddresses := input.InputAddresses() + if len(inputAddresses) != len(sigs) { + return sdk.ErrUnauthorized("Wrong number of confirm sigs").Result() } - inputAddresses := plasmaUTXO.GetInputAddresses() - // Get the block hash + // Get the block hash that input was created in blknumKey := make([]byte, binary.MaxVarintLen64) - binary.PutUvarint(blknumKey, plasmaUTXO.GetPosition().Get()[0].Uint64()) + binary.PutUvarint(blknumKey, input.Position.Get()[0].Uint64()) blockHash := metadataMapper.GetMetadata(ctx, blknumKey) - txHash := plasmaUTXO.GetTxHash() - - hash := append(txHash, blockHash...) + // Create confirm signature hash + hash := append(input.TxHash, blockHash...) confirmHash := tmhash.Sum(hash) signHash := utils.SignHash(confirmHash) - pubKey0, err0 := ethcrypto.SigToPub(signHash, sigs[0][:]) - if err0 != nil || !reflect.DeepEqual(ethcrypto.PubkeyToAddress(*pubKey0).Bytes(), inputAddresses[0].Bytes()) { - return sdk.ErrUnauthorized("confirm signature 0 verification failed").Result() - } - - if utils.ValidAddress(inputAddresses[1]) { - pubKey1, err1 := ethcrypto.SigToPub(signHash, sigs[1][:]) - if err1 != nil || !reflect.DeepEqual(ethcrypto.PubkeyToAddress(*pubKey1).Bytes(), inputAddresses[1].Bytes()) { - return sdk.ErrUnauthorized("confirm signature 1 verification failed").Result() + for i, sig := range sigs { + pubKey, err := ethcrypto.SigToPub(signHash, sig[:]) + if err != nil || !reflect.DeepEqual(ethcrypto.PubkeyToAddress(*pubKey).Bytes(), inputAddresses[i]) { + return sdk.ErrUnauthorized(fmt.Sprintf("confirm signature %d verification failed", i)).Result() } } @@ -164,14 +158,14 @@ func processConfirmSig( // Checks that utxo at the position specified exists, matches the address in the SpendMsg // and returns the denomination associated with the utxo func checkUTXO(ctx sdk.Context, mapper utxo.Mapper, position types.PlasmaPosition, addr common.Address) sdk.Result { - utxo := mapper.GetUTXO(ctx, addr.Bytes(), &position) - if utxo == nil { - return sdk.ErrUnknownRequest(fmt.Sprintf("UTXO trying to be spent, does not exist: %v.", position)).Result() + input := mapper.GetUTXO(ctx, addr.Bytes(), &position) + if !input.Valid { + return sdk.ErrUnknownRequest(fmt.Sprintf("UTXO trying to be spent, is not valid: %v.", position)).Result() } // Verify that utxo owner equals input address in the transaction - if !reflect.DeepEqual(utxo.GetAddress(), addr.Bytes()) { - return sdk.ErrUnauthorized(fmt.Sprintf("signer does not match utxo owner, signer: %X owner: %X", addr.Bytes(), utxo.GetAddress())).Result() + if !reflect.DeepEqual(input.Address, addr.Bytes()) { + return sdk.ErrUnauthorized(fmt.Sprintf("signer does not match utxo owner, signer: %X owner: %X", addr.Bytes(), input.Address)).Result() } return sdk.Result{} diff --git a/auth/ante_test.go b/auth/ante_test.go index 3401977..6ab165c 100644 --- a/auth/ante_test.go +++ b/auth/ante_test.go @@ -71,14 +71,15 @@ func CreateConfirmSig(hash []byte, privKey0, privKey1 *ecdsa.PrivateKey, two_inp signHash := utils.SignHash(hash) confirmSig0Slice, _ := ethcrypto.Sign(signHash, privKey0) copy(confirmSig0[:], confirmSig0Slice) + if !two_inputs { + return [][65]byte{confirmSig0} + } var confirmSig1 [65]byte - if two_inputs { - confirmSig1Slice, _ := ethcrypto.Sign(signHash, privKey1) - copy(confirmSig1[:], confirmSig1Slice) - } - confirmSigs = [][65]byte{confirmSig0, confirmSig1} - return confirmSigs + confirmSig1Slice, _ := ethcrypto.Sign(signHash, privKey1) + copy(confirmSig1[:], confirmSig1Slice) + + return [][65]byte{confirmSig0, confirmSig1} } // helper for constructing single or double input tx @@ -100,11 +101,11 @@ func GetTx(msg types.SpendMsg, privKey0, privKey1 *ecdsa.PrivateKey, two_sigs bo } // helper for constructing input addresses -func getInputAddr(addr0, addr1 common.Address, two bool) [2]common.Address { +func getInputAddr(addr0, addr1 common.Address, two bool) [][]byte { if two { - return [2]common.Address{addr0, addr1} + return [][]byte{addr0.Bytes(), addr1.Bytes()} } else { - return [2]common.Address{addr0, common.Address{}} + return [][]byte{addr0.Bytes()} } } @@ -117,10 +118,10 @@ func TestNoSigs(t *testing.T) { tx := types.NewBaseTx(msg, emptysigs) // Add input UTXOs to mapper - utxo1 := types.NewBaseUTXO(msg.Owner0, [2]common.Address{}, 100, types.Denom, types.NewPlasmaPosition(1, 0, 0, 0)) - utxo2 := types.NewBaseUTXO(msg.Owner0, [2]common.Address{}, 100, types.Denom, types.NewPlasmaPosition(1, 1, 0, 0)) - mapper.AddUTXO(ctx, utxo1) - mapper.AddUTXO(ctx, utxo2) + utxo1 := utxo.NewUTXO(msg.Owner0.Bytes(), 100, types.Denom, types.NewPlasmaPosition(1, 0, 0, 0)) + utxo2 := utxo.NewUTXO(msg.Owner0.Bytes(), 100, types.Denom, types.NewPlasmaPosition(1, 1, 0, 0)) + mapper.ReceiveUTXO(ctx, utxo1) + mapper.ReceiveUTXO(ctx, utxo2) handler := NewAnteHandler(mapper, metadataMapper, feeUpdater) _, res, abort := handler(ctx, tx, false) @@ -142,10 +143,10 @@ func TestNotEnoughSigs(t *testing.T) { tx := types.NewBaseTx(msg, sigs) // Add input UTXOs to mapper - utxo1 := types.NewBaseUTXO(msg.Owner0, [2]common.Address{}, 100, types.Denom, types.NewPlasmaPosition(1, 0, 0, 0)) - utxo2 := types.NewBaseUTXO(msg.Owner0, [2]common.Address{}, 100, types.Denom, types.NewPlasmaPosition(1, 1, 0, 0)) - mapper.AddUTXO(ctx, utxo1) - mapper.AddUTXO(ctx, utxo2) + utxo1 := utxo.NewUTXO(msg.Owner0.Bytes(), 100, types.Denom, types.NewPlasmaPosition(1, 0, 0, 0)) + utxo2 := utxo.NewUTXO(msg.Owner0.Bytes(), 100, types.Denom, types.NewPlasmaPosition(1, 1, 0, 0)) + mapper.ReceiveUTXO(ctx, utxo1) + mapper.ReceiveUTXO(ctx, utxo2) handler := NewAnteHandler(mapper, metadataMapper, feeUpdater) _, res, abort := handler(ctx, tx, false) @@ -256,17 +257,15 @@ func TestDifferentCases(t *testing.T) { require.Equal(t, sdk.ToABCICode(sdk.CodespaceType(1), sdk.CodeType(6)), res.Code, res.Log) inputAddr := getInputAddr(addrs[tc.input0.input_index0], addrs[input0_index1], tc.input0.input_index1 != -1) - utxo0 := types.NewBaseUTXO(tc.input0.addr, inputAddr, 2000, types.Denom, tc.input0.position) txhash0 := tmhash.Sum([]byte("first utxo")) - utxo0.TxHash = txhash0 + utxo0 := utxo.NewUTXOwithInputs(tc.input0.addr.Bytes(), 2000, types.Denom, tc.input0.position, txhash0, inputAddr) - var utxo1 *types.BaseUTXO + var utxo1 utxo.UTXO var txhash1 []byte if tc.input1.owner_index != -1 { txhash1 = tmhash.Sum([]byte("second utxo")) inputAddr = getInputAddr(addrs[input1_index0], addrs[input1_index1], tc.input0.input_index1 != -1) - utxo1 = types.NewBaseUTXO(tc.input1.addr, inputAddr, 2000, types.Denom, tc.input1.position) - utxo1.TxHash = txhash1 + utxo1 = utxo.NewUTXOwithInputs(tc.input1.addr.Bytes(), 2000, types.Denom, tc.input1.position, txhash1, inputAddr) } blknumKey := make([]byte, binary.MaxVarintLen64) @@ -275,12 +274,12 @@ func TestDifferentCases(t *testing.T) { // for ease of testing, txhash is simplified // app_test tests for correct functionality when setting tx_hash - mapper.AddUTXO(ctx, utxo0) + mapper.ReceiveUTXO(ctx, utxo0) hash := tmhash.Sum(append(txhash0, blockHash...)) msg.Input0ConfirmSigs = CreateConfirmSig(hash, keys[tc.input0.input_index0], keys[input0_index1], tc.input0.input_index1 != -1) if tc.input1.owner_index != -1 { hash = tmhash.Sum(append(txhash1, blockHash...)) - mapper.AddUTXO(ctx, utxo1) + mapper.ReceiveUTXO(ctx, utxo1) msg.Input1ConfirmSigs = CreateConfirmSig(hash, keys[input1_index0], keys[input1_index1], tc.input1.input_index1 != -1) } tx = GetTx(msg, keys[tc.input0.owner_index], keys[owner_index1], tc.input1.owner_index != -1) diff --git a/client/plasmacli/cmd/balance.go b/client/plasmacli/cmd/balance.go index 53793eb..20b603f 100644 --- a/client/plasmacli/cmd/balance.go +++ b/client/plasmacli/cmd/balance.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/FourthState/plasma-mvp-sidechain/client" "github.com/FourthState/plasma-mvp-sidechain/client/context" - "github.com/FourthState/plasma-mvp-sidechain/types" + "github.com/FourthState/plasma-mvp-sidechain/x/utxo" "github.com/ethereum/go-ethereum/common" "github.com/spf13/cobra" ) @@ -30,12 +30,14 @@ var balanceCmd = &cobra.Command{ } for _, pair := range res { - var utxo types.BaseUTXO - err := ctx.Codec.UnmarshalBinaryBare(pair.Value, &utxo) + var resUTXO utxo.UTXO + err := ctx.Codec.UnmarshalBinaryBare(pair.Value, &resUTXO) if err != nil { return err } - fmt.Printf("Position: %v \nAmount: %d \n", utxo.Position, utxo.Amount) + if resUTXO.Valid { + fmt.Printf("Position: %v \nAmount: %d \n", resUTXO.Position, resUTXO.Amount) + } } return nil diff --git a/client/plasmacli/cmd/confirmSig.go b/client/plasmacli/cmd/confirmSig.go index ff37203..2bcf5a4 100644 --- a/client/plasmacli/cmd/confirmSig.go +++ b/client/plasmacli/cmd/confirmSig.go @@ -10,8 +10,8 @@ import ( "github.com/FourthState/plasma-mvp-sidechain/client" "github.com/FourthState/plasma-mvp-sidechain/client/context" - "github.com/FourthState/plasma-mvp-sidechain/types" "github.com/FourthState/plasma-mvp-sidechain/utils" + "github.com/FourthState/plasma-mvp-sidechain/x/utxo" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" "github.com/tendermint/tendermint/crypto/tmhash" @@ -66,18 +66,18 @@ var signCmd = &cobra.Command{ return err } - var utxo types.BaseUTXO - err = ctx.Codec.UnmarshalBinaryBare(res, &utxo) + var input utxo.UTXO + err = ctx.Codec.UnmarshalBinaryBare(res, &input) blknumKey := make([]byte, binary.MaxVarintLen64) - binary.PutUvarint(blknumKey, utxo.GetPosition().Get()[0].Uint64()) + binary.PutUvarint(blknumKey, input.Position.Get()[0].Uint64()) blockhash, err := ctx.QueryStore(blknumKey, ctx.MetadataStore) if err != nil { return err } - hash := tmhash.Sum(append(utxo.TxHash, blockhash...)) + hash := tmhash.Sum(append(input.TxHash, blockhash...)) signHash := utils.SignHash(hash) dir := viper.GetString(FlagHomeDir) @@ -100,15 +100,10 @@ var signCmd = &cobra.Command{ sig, err := ks.SignHashWithPassphrase(acct, passphrase, signHash) - fmt.Printf("\nConfirmation Signature for utxo with\nposition: %v \namount: %d\n", utxo.Position, utxo.Amount) + fmt.Printf("\nConfirmation Signature for utxo with\nposition: %v \namount: %d\n", input.Position, input.Amount) fmt.Printf("signature: %x\n", sig) - inputLen := 1 - // check number of inputs - if !utils.ZeroAddress(utxo.InputAddresses[1]) { - inputLen = 2 - } - fmt.Printf("UTXO had %d inputs\n", inputLen) + fmt.Printf("UTXO had %d inputs\n", len(input.InputAddresses())) return nil }, } diff --git a/client/plasmacli/cmd/info.go b/client/plasmacli/cmd/info.go index bc3c43f..0cc14ed 100644 --- a/client/plasmacli/cmd/info.go +++ b/client/plasmacli/cmd/info.go @@ -1,11 +1,12 @@ package cmd import ( + "encoding/hex" "fmt" "github.com/FourthState/plasma-mvp-sidechain/client" "github.com/FourthState/plasma-mvp-sidechain/client/context" "github.com/FourthState/plasma-mvp-sidechain/types" - "github.com/FourthState/plasma-mvp-sidechain/utils" + "github.com/FourthState/plasma-mvp-sidechain/x/utxo" "github.com/ethereum/go-ethereum/common" "github.com/spf13/cobra" ) @@ -18,7 +19,7 @@ func init() { var infoCmd = &cobra.Command{ Use: "info <address>", - Short: "Information on owned utxos ", + Short: "Information on owned utxos valid and invalid", RunE: func(cmd *cobra.Command, args []string) error { ctx := context.NewClientContextFromViper() @@ -29,25 +30,40 @@ var infoCmd = &cobra.Command{ return err2 } - for _, pair := range res { - var utxo types.BaseUTXO - err := ctx.Codec.UnmarshalBinaryBare(pair.Value, &utxo) + for i, pair := range res { + fmt.Printf("Start UTXO %d info:\n", i) + var resUTXO utxo.UTXO + err := ctx.Codec.UnmarshalBinaryBare(pair.Value, &resUTXO) if err != nil { return err } - fmt.Printf("\nPosition: %v \nAmount: %d \nDenomination: %s \n", utxo.Position, utxo.Amount, utxo.GetDenom()) - inputAddrHelper(utxo) + fmt.Printf("\nPosition: %v \nAmount: %d \nDenomination: %s \nValid: %t\n", resUTXO.Position, resUTXO.Amount, resUTXO.Denom, resUTXO.Valid) + + if resUTXO.InputKeys != nil { + inputOwners := resUTXO.InputAddresses() + inputs := resUTXO.InputPositions(ctx.Codec, types.ProtoPosition) + for i, key := range resUTXO.InputKeys { + plasmaInput, _ := inputs[i].(*types.PlasmaPosition) + fmt.Printf("\nInput Owner %d: %s\nInput Position %d: %v\nInputKey %d in UTXO store: %s\n", i, hex.EncodeToString(inputOwners[i]), + i, *plasmaInput, i, hex.EncodeToString(key)) + } + } + + if resUTXO.SpenderKeys != nil { + spenders := resUTXO.SpenderAddresses() + spenderPositions := resUTXO.SpenderPositions(ctx.Codec, types.ProtoPosition) + for i, key := range resUTXO.SpenderKeys { + plasmaPosition, _ := spenderPositions[i].(*types.PlasmaPosition) + fmt.Printf("\nSpender %d: %s\nSpending Position %d: %v\nSpendKey %d in UTXO store: %s\n", i, hex.EncodeToString(spenders[i]), + i, *plasmaPosition, i, hex.EncodeToString(key)) + } + } + + fmt.Printf("End UTXO %d info:\n", i) } + fmt.Println() + return nil }, } - -func inputAddrHelper(utxo types.BaseUTXO) { - if utxo.Position.DepositNum == 0 { - fmt.Printf("First Input Address: %s \n", utxo.InputAddresses[0].Hex()) - if !utils.ZeroAddress(utxo.InputAddresses[1]) { - fmt.Printf("Second Input Address: %s \n", utxo.InputAddresses[1].Hex()) - } - } -} diff --git a/client/plasmacli/cmd/prove.go b/client/plasmacli/cmd/prove.go index a022b31..9296b08 100644 --- a/client/plasmacli/cmd/prove.go +++ b/client/plasmacli/cmd/prove.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/FourthState/plasma-mvp-sidechain/client" "github.com/FourthState/plasma-mvp-sidechain/client/context" - "github.com/FourthState/plasma-mvp-sidechain/types" + "github.com/FourthState/plasma-mvp-sidechain/x/utxo" "github.com/ethereum/go-ethereum/common" "github.com/spf13/cobra" ) @@ -41,13 +41,13 @@ var proveCmd = &cobra.Command{ return err2 } - var utxo types.BaseUTXO - err = ctx.Codec.UnmarshalBinaryBare(res, &utxo) + var resUTXO utxo.UTXO + err = ctx.Codec.UnmarshalBinaryBare(res, &resUTXO) if err != nil { return err } - result, err := ctx.Client.Tx(utxo.TxHash, true) + result, err := ctx.Client.Tx(resUTXO.TxHash, true) if err != nil { return err } diff --git a/types/utxo.go b/types/utxo.go index 483bf40..cb75945 100644 --- a/types/utxo.go +++ b/types/utxo.go @@ -1,16 +1,10 @@ package types import ( - "errors" - "fmt" amino "github.com/tendermint/go-amino" - utils "github.com/FourthState/plasma-mvp-sidechain/utils" "github.com/FourthState/plasma-mvp-sidechain/x/utxo" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/ethereum/go-ethereum/common" - - "github.com/tendermint/tendermint/crypto/tmhash" ) const ( @@ -18,126 +12,9 @@ const ( Denom = "Ether" ) -var _ utxo.UTXO = &BaseUTXO{} - -// Implements UTXO interface -type BaseUTXO struct { - InputAddresses [2]common.Address - Address common.Address - Amount uint64 - Denom string - Position PlasmaPosition - TxHash []byte -} - -func ProtoUTXO(ctx sdk.Context, msg sdk.Msg) utxo.UTXO { - spendmsg, ok := msg.(SpendMsg) - if !ok { - return nil - } - - return &BaseUTXO{ - InputAddresses: [2]common.Address{spendmsg.Owner0, spendmsg.Owner1}, - TxHash: tmhash.Sum(ctx.TxBytes()), - } -} - -func NewBaseUTXO(addr common.Address, inputaddr [2]common.Address, amount uint64, - denom string, position PlasmaPosition) *BaseUTXO { - return &BaseUTXO{ - InputAddresses: inputaddr, - Address: addr, - Amount: amount, - Denom: denom, - Position: position, - } -} - -func (baseutxo BaseUTXO) GetTxHash() []byte { - return baseutxo.TxHash -} - -//Implements UTXO -func (baseutxo BaseUTXO) GetAddress() []byte { - return baseutxo.Address.Bytes() -} - -//Implements UTXO -func (baseutxo *BaseUTXO) SetAddress(addr []byte) error { - if !utils.ZeroAddress(baseutxo.Address) { - return fmt.Errorf("address already set to: %X", baseutxo.Address) - } - address := common.BytesToAddress(addr) - if utils.ZeroAddress(address) { - return fmt.Errorf("invalid address provided: %X", address) - } - baseutxo.Address = address - return nil -} - -//Implements UTXO -func (baseutxo *BaseUTXO) SetInputAddresses(addrs [2]common.Address) error { - if !utils.ZeroAddress(baseutxo.InputAddresses[0]) { - return fmt.Errorf("input addresses already set to: %X, %X", baseutxo.InputAddresses[0], baseutxo.InputAddresses[1]) - } - if utils.ZeroAddress(addrs[0]) { - return fmt.Errorf("invalid address provided: %X", addrs[0]) - } - baseutxo.InputAddresses = addrs - return nil -} - -//Implements UTXO -func (baseutxo BaseUTXO) GetInputAddresses() [2]common.Address { - return baseutxo.InputAddresses -} - -//Implements UTXO -func (baseutxo BaseUTXO) GetAmount() uint64 { - return baseutxo.Amount -} - -//Implements UTXO -func (baseutxo *BaseUTXO) SetAmount(amount uint64) error { - if baseutxo.Amount != 0 { - return fmt.Errorf("amount already set to: %d", baseutxo.Amount) - } - baseutxo.Amount = amount - return nil -} - -func (baseutxo BaseUTXO) GetPosition() utxo.Position { - return baseutxo.Position -} - -func (baseutxo *BaseUTXO) SetPosition(position utxo.Position) error { - if baseutxo.Position.IsValid() { - return fmt.Errorf("position already set to: %v", baseutxo.Position) - } else if !position.IsValid() { - return errors.New("invalid position provided") - } - - plasmaposition, ok := position.(PlasmaPosition) - if !ok { - return errors.New("position must be of type PlasmaPosition") - } - baseutxo.Position = plasmaposition - return nil -} - -func (baseutxo BaseUTXO) GetDenom() string { - return Denom -} - -func (baseutxo *BaseUTXO) SetDenom(denom string) error { - return nil -} - //---------------------------------------- // Position -var _ utxo.Position = &PlasmaPosition{} - type PlasmaPosition struct { Blknum uint64 TxIndex uint16 @@ -145,6 +22,10 @@ type PlasmaPosition struct { DepositNum uint64 } +func ProtoPosition() utxo.Position { + return &PlasmaPosition{} +} + func NewPlasmaPosition(blknum uint64, txIndex uint16, oIndex uint8, depositNum uint64) PlasmaPosition { return PlasmaPosition{ Blknum: blknum, @@ -173,8 +54,7 @@ func (position PlasmaPosition) IsValid() bool { //------------------------------------------------------- // misc func RegisterAmino(cdc *amino.Codec) { - cdc.RegisterConcrete(&BaseUTXO{}, "types/BaseUTXO", nil) - cdc.RegisterConcrete(&PlasmaPosition{}, "types/PlasmaPosition", nil) + cdc.RegisterConcrete(PlasmaPosition{}, "types/PlasmaPosition", nil) cdc.RegisterConcrete(BaseTx{}, "types/BaseTX", nil) cdc.RegisterConcrete(SpendMsg{}, "types/SpendMsg", nil) } diff --git a/types/utxo_test.go b/types/utxo_test.go deleted file mode 100644 index 57e58fb..0000000 --- a/types/utxo_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package types - -import ( - "fmt" - "github.com/stretchr/testify/require" - "testing" - - "github.com/FourthState/plasma-mvp-sidechain/utils" - "github.com/ethereum/go-ethereum/common" - ethcrypto "github.com/ethereum/go-ethereum/crypto" -) - -// return a base utxo with nothing set, along with two addresses -func GetBareUTXO() (utxo *BaseUTXO, addrA, addrB common.Address) { - privKeyA, _ := ethcrypto.GenerateKey() - privKeyB, _ := ethcrypto.GenerateKey() - addrA = utils.PrivKeyToAddress(privKeyA) - addrB = utils.PrivKeyToAddress(privKeyB) - return &BaseUTXO{}, addrA, addrB -} - -// Basic tests checking methods for BaseUTXO -func TestGetSetAddress(t *testing.T) { - utxo, addrA, addrB := GetBareUTXO() - - // try to set address to another blank address - err := utxo.SetAddress(common.Address{}.Bytes()) - require.Error(t, err) - - // set address to addrB - err = utxo.SetAddress(addrB.Bytes()) - require.NoError(t, err) - - // try to set address to addrA (currently set to addrB) - err = utxo.SetAddress(addrA.Bytes()) - require.Error(t, err) - - // check get method - addr := utxo.GetAddress() - require.Equal(t, addr, addrB.Bytes(), fmt.Sprintf("BaseUTXO GetAddress() method returned the wrong address: %s", addr)) -} - -// Test GetInputAddresses() and SetInputAddresses -func TestInputAddresses(t *testing.T) { - utxo, addrA, addrB := GetBareUTXO() - - // try to set input address to blank addresses - err := utxo.SetInputAddresses([2]common.Address{common.Address{}, common.Address{}}) - require.Error(t, err) - - // set input addresses to addrA, addrA - err = utxo.SetInputAddresses([2]common.Address{addrA, addrA}) - require.NoError(t, err) - - // try to set input address to addrB - err = utxo.SetInputAddresses([2]common.Address{addrB, common.Address{}}) - require.Error(t, err) - - // check get method - addrs := utxo.GetInputAddresses() - require.Equal(t, addrs, [2]common.Address{addrA, addrA}) -} - -// Test GetAmount() and SetAmount() -func TestAmount(t *testing.T) { - utxo := NewBaseUTXO(common.Address{}, [2]common.Address{common.Address{}, common.Address{}}, 100, "ether", PlasmaPosition{}) - - // try to set denom when it already has a value - err := utxo.SetAmount(100000000) - require.Error(t, err) - - // check get method - amount := utxo.GetAmount() - require.Equal(t, amount, uint64(100), "the wrong amount was returned by GetAmount()") -} - -// Test GetPosition() and SetPosition() -func TestPosition(t *testing.T) { - utxo := BaseUTXO{} - position := NewPlasmaPosition(0, uint16(0), uint8(0), 0) - - // try to set position to incorrect position 0, 0, 0, 0 - err := utxo.SetPosition(position) - require.Error(t, err) - - // set position - position = NewPlasmaPosition(5, uint16(12), uint8(1), 0) - err = utxo.SetPosition(position) - require.NoError(t, err) - - // try to set to different position - position = NewPlasmaPosition(1, uint16(23), uint8(1), 0) - err = utxo.SetPosition(position) - require.Error(t, err) - - // check get method - require.Equal(t, utxo.GetPosition(), NewPlasmaPosition(5, uint16(12), uint8(1), 0), "the wrong position was returned") -} diff --git a/x/utxo/handler.go b/x/utxo/handler.go index 4573fe8..32515c5 100644 --- a/x/utxo/handler.go +++ b/x/utxo/handler.go @@ -2,6 +2,7 @@ package utxo import ( sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/tendermint/tendermint/crypto/tmhash" ) // Return the next position for handler to store newly created UTXOs @@ -9,25 +10,27 @@ import ( // If false, NextPosition will increment position to accomadate outputs for a new transaction type NextPosition func(ctx sdk.Context, secondary bool) Position -// Proto function to create application's UTXO implementation and fill with some proto-information -type ProtoUTXO func(sdk.Context, sdk.Msg) UTXO - // User-defined fee update function type FeeUpdater func([]Output) sdk.Error // Handler handles spends of arbitrary utxo implementation -func NewSpendHandler(um Mapper, nextPos NextPosition, proto ProtoUTXO) sdk.Handler { +func NewSpendHandler(um Mapper, nextPos NextPosition) sdk.Handler { return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { spendMsg, ok := msg.(SpendMsg) if !ok { panic("Msg does not implement SpendMsg") } - // Delete inputs from store - for _, i := range spendMsg.Inputs() { - um.DeleteUTXO(ctx, i.Owner, i.Position) + var inputKeys [][]byte + inputs := spendMsg.Inputs() + for _, in := range inputs { + inKey := um.ConstructKey(in.Owner, in.Position) + inputKeys = append(inputKeys, inKey) } + txHash := tmhash.Sum(ctx.TxBytes()) + + var spenderKeys [][]byte // Add outputs from store for i, o := range spendMsg.Outputs() { var next Position @@ -36,12 +39,17 @@ func NewSpendHandler(um Mapper, nextPos NextPosition, proto ProtoUTXO) sdk.Handl } else { next = nextPos(ctx, true) } - utxo := proto(ctx, msg) - utxo.SetPosition(next) - utxo.SetAddress(o.Owner) - utxo.SetDenom(o.Denom) - utxo.SetAmount(o.Amount) - um.AddUTXO(ctx, utxo) + spenderKeys = append(spenderKeys, um.ConstructKey(o.Owner, next)) + utxo := NewUTXOwithInputs(o.Owner, o.Amount, o.Denom, next, txHash, inputKeys) + um.ReceiveUTXO(ctx, utxo) + } + + // Spend inputs from store + for _, i := range spendMsg.Inputs() { + err := um.SpendUTXO(ctx, i.Owner, i.Position, spenderKeys) + if err != nil { + return err.Result() + } } return sdk.Result{} @@ -61,7 +69,7 @@ func AnteHelper(ctx sdk.Context, um Mapper, tx sdk.Tx, simulate bool, feeUpdater totalInput := map[string]uint64{} for _, i := range spendMsg.Inputs() { utxo := um.GetUTXO(ctx, i.Owner, i.Position) - totalInput[utxo.GetDenom()] += utxo.GetAmount() + totalInput[utxo.Denom] += utxo.Amount } // Add up all outputs and fee diff --git a/x/utxo/handler_test.go b/x/utxo/handler_test.go index 845afff..f9793e2 100644 --- a/x/utxo/handler_test.go +++ b/x/utxo/handler_test.go @@ -103,30 +103,35 @@ func TestHandleSpendMessage(t *testing.T) { ms, capKey, _ := SetupMultiStore() cdc := MakeCodec() - cdc.RegisterConcrete(&testUTXO{}, "x/utxo/testUTXO", nil) + cdc.RegisterConcrete(&UTXO{}, "x/utxo/UTXO", nil) cdc.RegisterConcrete(&testPosition{}, "x/utxo/testPosition", nil) mapper := NewBaseMapper(capKey, cdc) app := testApp{0, 0} - handler := NewSpendHandler(mapper, app.testNextPosition, testProtoUTXO) + handler := NewSpendHandler(mapper, app.testNextPosition) ctx := sdk.NewContext(ms, abci.Header{Height: 6}, false, log.NewNopLogger()) var inputs []Input var outputs []Output + var inputKeys [][]byte + var spenderKeys [][]byte // Add utxo's that will be spent for i := 0; i < tc.inputNum; i++ { position := newTestPosition([]uint64{5, uint64(i), 0}) - utxo := newTestUTXO(addrs[i].Bytes(), 100, position) - mapper.AddUTXO(ctx, utxo) + utxo := NewUTXO(addrs[i].Bytes(), 100, "testEther", position) + mapper.ReceiveUTXO(ctx, utxo) utxo = mapper.GetUTXO(ctx, addrs[i].Bytes(), position) require.NotNil(t, utxo) inputs = append(inputs, Input{addrs[i].Bytes(), position}) + inputKeys = append(inputKeys, mapper.ConstructKey(addrs[i].Bytes(), position)) } for i := 0; i < tc.outputNum; i++ { outputs = append(outputs, Output{addrs[(i+1)%len].Bytes(), "Ether", uint64((100 * tc.inputNum) / tc.outputNum)}) + position := newTestPosition([]uint64{6, 0, uint64(i)}) + spenderKeys = append(spenderKeys, mapper.ConstructKey(addrs[(i+1)%len].Bytes(), position)) } // Create spend msg @@ -141,9 +146,10 @@ func TestHandleSpendMessage(t *testing.T) { // Delete inputs for _, in := range msg.Inputs() { - mapper.DeleteUTXO(ctx, in.Owner, in.Position) utxo := mapper.GetUTXO(ctx, in.Owner, in.Position) - require.Nil(t, utxo) + require.NotNil(t, utxo) + require.False(t, utxo.Valid, "Spent UTXO not valid") + require.Equal(t, spenderKeys, utxo.SpenderKeys, "Spender keys not set properly for inputs") } // Check that outputs were created and are valid @@ -153,12 +159,10 @@ func TestHandleSpendMessage(t *testing.T) { utxo := mapper.GetUTXO(ctx, o.Owner, position) require.NotNil(t, utxo, fmt.Sprintf("test case %d, output %d", index, i)) - require.Equal(t, uint64((tc.inputNum*100)/tc.outputNum), utxo.GetAmount()) - require.EqualValues(t, addrs[(i+1)%len].Bytes(), utxo.GetAddress()) - - mapper.DeleteUTXO(ctx, o.Owner, position) - utxo = mapper.GetUTXO(ctx, o.Owner, position) - require.Nil(t, utxo) + require.Equal(t, uint64((tc.inputNum*100)/tc.outputNum), utxo.Amount) + require.EqualValues(t, addrs[(i+1)%len].Bytes(), utxo.Address) + require.True(t, utxo.Valid, "Output UTXO is not valid") + require.Equal(t, inputKeys, utxo.InputKeys, "Input keys for new outputs not set properly") } } } diff --git a/x/utxo/mapper.go b/x/utxo/mapper.go index 4bc3d93..3b3a791 100644 --- a/x/utxo/mapper.go +++ b/x/utxo/mapper.go @@ -9,16 +9,17 @@ import ( // retrieved from the context. type Mapper interface { GetUTXO(ctx sdk.Context, addr []byte, position Position) UTXO - GetUTXOsForAddress(ctx sdk.Context, addr []byte) []UTXO - AddUTXO(ctx sdk.Context, utxo UTXO) - DeleteUTXO(ctx sdk.Context, addr []byte, position Position) + ConstructKey(addr []byte, position Position) []byte + ReceiveUTXO(sdk.Context, UTXO) + ValidateUTXO(sdk.Context, UTXO) sdk.Error + InvalidateUTXO(sdk.Context, UTXO) + SpendUTXO(ctx sdk.Context, addr []byte, position Position, spenderKeys [][]byte) sdk.Error } // Maps Address+Position to UTXO // Uses go-amino encoding/decoding library // Implements Mapper type baseMapper struct { - // The contextKey used to access the store from the Context. contextKey sdk.StoreKey @@ -31,60 +32,71 @@ func NewBaseMapper(contextKey sdk.StoreKey, cdc *amino.Codec) Mapper { contextKey: contextKey, cdc: cdc, } - } // Returns the UTXO corresponding to the address + go amino encoded Position struct // Returns nil if no UTXO exists at that position func (um baseMapper) GetUTXO(ctx sdk.Context, addr []byte, position Position) UTXO { store := ctx.KVStore(um.contextKey) - key := um.constructKey(addr, position) + key := um.ConstructKey(addr, position) bz := store.Get(key) if bz == nil { - return nil + return UTXO{} } utxo := um.decodeUTXO(bz) return utxo } -// Returns all the UTXOs owned by an address. -// Returns empty slice if no UTXO exists for the address. -func (um baseMapper) GetUTXOsForAddress(ctx sdk.Context, addr []byte) []UTXO { +// Receives the UTXO to the mapper +func (um baseMapper) ReceiveUTXO(ctx sdk.Context, utxo UTXO) { store := ctx.KVStore(um.contextKey) - iterator := sdk.KVStorePrefixIterator(store, addr) - utxos := make([]UTXO, 0) - - for ; iterator.Valid(); iterator.Next() { - utxo := um.decodeUTXO(iterator.Value()) - utxos = append(utxos, utxo) - } - iterator.Close() - return utxos + key := utxo.StoreKey(um.cdc) + bz := um.encodeUTXO(utxo) + store.Set(key, bz) } -// Adds the UTXO to the mapper -func (um baseMapper) AddUTXO(ctx sdk.Context, utxo UTXO) { - position := utxo.GetPosition() - address := utxo.GetAddress() +// Spend UTXO corresponding to address + position from mapping +func (um baseMapper) SpendUTXO(ctx sdk.Context, addr []byte, position Position, spenderKeys [][]byte) sdk.Error { store := ctx.KVStore(um.contextKey) + key := um.ConstructKey(addr, position) + utxo := um.GetUTXO(ctx, addr, position) + if !utxo.Valid { + return sdk.ErrUnauthorized("UTXO is not valid for spend") + } + utxo.Valid = false + utxo.SpenderKeys = spenderKeys + encodedUTXO := um.encodeUTXO(utxo) + store.Set(key, encodedUTXO) + return nil +} - key := um.constructKey(address, position) - bz := um.encodeUTXO(utxo) - store.Set(key, bz) +// Validates UTXO only if it not spent already +func (um baseMapper) ValidateUTXO(ctx sdk.Context, utxo UTXO) sdk.Error { + store := ctx.KVStore(um.contextKey) + key := utxo.StoreKey(um.cdc) + if utxo.SpenderKeys != nil { + return sdk.ErrUnauthorized("Cannot validate spent UTXO") + } + utxo.Valid = true + encodedUTXO := um.encodeUTXO(utxo) + store.Set(key, encodedUTXO) + return nil } -// Deletes UTXO corresponding to address + position from mapping -func (um baseMapper) DeleteUTXO(ctx sdk.Context, addr []byte, position Position) { +// Invalidates UTXO +func (um baseMapper) InvalidateUTXO(ctx sdk.Context, utxo UTXO) { store := ctx.KVStore(um.contextKey) - key := um.constructKey(addr, position) - store.Delete(key) + key := utxo.StoreKey(um.cdc) + utxo.Valid = false + encodedUTXO := um.encodeUTXO(utxo) + store.Set(key, encodedUTXO) } // (<address> + <encoded position>) forms the unique key that maps to an UTXO. -func (um baseMapper) constructKey(address []byte, position Position) []byte { +func (um baseMapper) ConstructKey(address []byte, position Position) []byte { posBytes, err := um.cdc.MarshalBinaryBare(position) if err != nil { panic(err) diff --git a/x/utxo/mapper_test.go b/x/utxo/mapper_test.go index 5de727c..83023fd 100644 --- a/x/utxo/mapper_test.go +++ b/x/utxo/mapper_test.go @@ -1,8 +1,6 @@ package utxo import ( - "errors" - "fmt" "github.com/stretchr/testify/require" "testing" @@ -23,8 +21,8 @@ type testPosition struct { OutputIndex uint8 } -func testProtoUTXO(ctx sdk.Context, msg sdk.Msg) UTXO { - return &testUTXO{} +func testProtoPosition() Position { + return &testPosition{} } func newTestPosition(newPos []uint64) testPosition { @@ -46,89 +44,19 @@ func (pos testPosition) IsValid() bool { return true } -var _ UTXO = &testUTXO{} - -type testUTXO struct { - Owner []byte - Amount uint64 - Denom string - Position testPosition -} - -func newTestUTXO(owner []byte, amount uint64, position testPosition) UTXO { - return &testUTXO{ - Owner: owner, - Amount: amount, - Denom: "Ether", - Position: position, - } -} - -func (utxo testUTXO) GetAddress() []byte { - return utxo.Owner -} - -func (utxo *testUTXO) SetAddress(address []byte) error { - if utxo.Owner != nil { - return errors.New("Owner already set") - } - utxo.Owner = address - return nil -} - -func (utxo testUTXO) GetAmount() uint64 { - return utxo.Amount -} - -func (utxo *testUTXO) SetAmount(amount uint64) error { - if utxo.Amount != 0 { - return errors.New("Owner already set") - } - utxo.Amount = amount - return nil -} - -func (utxo testUTXO) GetDenom() string { - return utxo.Denom -} - -func (utxo *testUTXO) SetDenom(denom string) error { - if utxo.Denom != "" { - return errors.New("Owner already set") - } - utxo.Denom = denom - return nil -} - -func (utxo testUTXO) GetPosition() Position { - return utxo.Position -} - -func (utxo *testUTXO) SetPosition(pos Position) error { - - position, ok := pos.(testPosition) - if !ok { - fmt.Println("ah") - return errors.New("position setting err") - } - utxo.Position = position - return nil -} - /* - Basic test of Get, Add, Delete + Basic test of Get, Receive, Spend Creates a valid UTXO and adds it to the uxto mapping. Checks to make sure UTXO isn't nil after adding to mapping. Then deletes the UTXO from the mapping */ -func TestUTXOGetAddDelete(t *testing.T) { +func TestUTXOGetReceiveSpend(t *testing.T) { ms, capKey, _ := SetupMultiStore() ctx := sdk.NewContext(ms, abci.Header{}, false, log.NewNopLogger()) cdc := MakeCodec() - cdc.RegisterConcrete(&testUTXO{}, "x/utxo/testUTXO", nil) - cdc.RegisterConcrete(&testPosition{}, "x/utxo/testPosition", nil) + cdc.RegisterConcrete(testPosition{}, "x/utxo/testPosition", nil) mapper := NewBaseMapper(capKey, cdc) priv, _ := ethcrypto.GenerateKey() @@ -136,20 +64,21 @@ func TestUTXOGetAddDelete(t *testing.T) { position := newTestPosition([]uint64{1, 0, 0}) - utxo := newTestUTXO(addr.Bytes(), 100, position) + utxo := NewUTXO(addr.Bytes(), 100, "testEther", position) - require.NotNil(t, utxo) - require.Equal(t, addr.Bytes(), utxo.GetAddress()) - require.EqualValues(t, position, utxo.GetPosition()) + require.Equal(t, addr.Bytes(), utxo.Address) + require.EqualValues(t, position, utxo.Position) - mapper.AddUTXO(ctx, utxo) + mapper.ReceiveUTXO(ctx, utxo) - utxo = mapper.GetUTXO(ctx, addr.Bytes(), position) - require.NotNil(t, utxo) + received := mapper.GetUTXO(ctx, addr.Bytes(), position) + require.True(t, received.Valid, "output UTXO is not valid") + require.Equal(t, utxo, received, "not equal after receive") - mapper.DeleteUTXO(ctx, addr.Bytes(), position) + mapper.SpendUTXO(ctx, addr.Bytes(), position, [][]byte{[]byte("spenderKey")}) utxo = mapper.GetUTXO(ctx, addr.Bytes(), position) - require.Nil(t, utxo) + require.False(t, utxo.Valid, "Spent UTXO is still valid") + require.Equal(t, utxo.SpenderKeys, [][]byte{[]byte("spenderKey")}) } /* @@ -163,8 +92,7 @@ func TestMultiUTXOAddDeleteSameBlock(t *testing.T) { ctx := sdk.NewContext(ms, abci.Header{}, false, log.NewNopLogger()) cdc := MakeCodec() - cdc.RegisterConcrete(&testUTXO{}, "x/utxo/testUTXO", nil) - cdc.RegisterConcrete(&testPosition{}, "x/utxo/testPosition", nil) + cdc.RegisterConcrete(testPosition{}, "x/utxo/testPosition", nil) mapper := NewBaseMapper(capKey, cdc) priv, _ := ethcrypto.GenerateKey() @@ -172,21 +100,21 @@ func TestMultiUTXOAddDeleteSameBlock(t *testing.T) { for i := 0; i < 20; i++ { position := newTestPosition([]uint64{uint64(i%4) + 1, uint64(i / 4), 0}) - utxo := newTestUTXO(addr.Bytes(), 100, position) - mapper.AddUTXO(ctx, utxo) + utxo := NewUTXO(addr.Bytes(), 100, "testEther", position) + mapper.ReceiveUTXO(ctx, utxo) utxo = mapper.GetUTXO(ctx, addr.Bytes(), position) - require.NotNil(t, utxo) + require.True(t, utxo.Valid, "Received UTXO is not valid") } for i := 0; i < 20; i++ { position := newTestPosition([]uint64{uint64(i%4) + 1, uint64(i / 4), 0}) utxo := mapper.GetUTXO(ctx, addr.Bytes(), position) - require.NotNil(t, utxo) - mapper.DeleteUTXO(ctx, addr.Bytes(), position) + mapper.SpendUTXO(ctx, addr.Bytes(), position, [][]byte{[]byte("spenderKey")}) utxo = mapper.GetUTXO(ctx, addr.Bytes(), position) - require.Nil(t, utxo) + require.False(t, utxo.Valid, "Spent UTXO is still valid") + require.Equal(t, utxo.SpenderKeys, [][]byte{[]byte("spenderKey")}) } } @@ -197,8 +125,7 @@ func TestInvalidAddress(t *testing.T) { ctx := sdk.NewContext(ms, abci.Header{}, false, log.NewNopLogger()) cdc := MakeCodec() - cdc.RegisterConcrete(&testUTXO{}, "x/utxo/testUTXO", nil) - cdc.RegisterConcrete(&testPosition{}, "x/utxo/testPosition", nil) + cdc.RegisterConcrete(testPosition{}, "x/utxo/testPosition", nil) mapper := NewBaseMapper(capKey, cdc) priv0, _ := ethcrypto.GenerateKey() @@ -209,82 +136,88 @@ func TestInvalidAddress(t *testing.T) { position := newTestPosition([]uint64{1, 0, 0}) - utxo := newTestUTXO(addr0.Bytes(), 100, position) + utxo := NewUTXO(addr0.Bytes(), 100, "testEther", position) - require.NotNil(t, utxo) - require.Equal(t, addr0.Bytes(), utxo.GetAddress()) - require.EqualValues(t, position, utxo.GetPosition()) + require.Equal(t, addr0.Bytes(), utxo.Address) + require.EqualValues(t, position, utxo.Position) - mapper.AddUTXO(ctx, utxo) + mapper.ReceiveUTXO(ctx, utxo) // GetUTXO with correct position but wrong address utxo = mapper.GetUTXO(ctx, addr1.Bytes(), position) - require.Nil(t, utxo) + require.Equal(t, utxo, UTXO{}, "Valid UTXO in wrong location") utxo = mapper.GetUTXO(ctx, addr0.Bytes(), position) - require.NotNil(t, utxo) - // DeleteUTXO with correct position but wrong address - mapper.DeleteUTXO(ctx, addr1.Bytes(), position) + // SpendUTXO with correct position but wrong address + mapper.SpendUTXO(ctx, addr1.Bytes(), position, [][]byte{[]byte("spenderKey")}) utxo = mapper.GetUTXO(ctx, addr0.Bytes(), position) - require.NotNil(t, utxo) + require.True(t, utxo.Valid, "UTXO invalid after invalid spend") + require.Nil(t, utxo.SpenderKeys, "UTXO has spenderKeys set after invalid spend") - mapper.DeleteUTXO(ctx, addr0.Bytes(), position) + mapper.SpendUTXO(ctx, addr0.Bytes(), position, [][]byte{[]byte("spenderKey")}) utxo = mapper.GetUTXO(ctx, addr0.Bytes(), position) - require.Nil(t, utxo) + require.False(t, utxo.Valid, "UTXO still valid after valid spend") + require.Equal(t, utxo.SpenderKeys, [][]byte{[]byte("spenderKey")}, "UTXO doesn't have spenderKeys set after valid spend") } -/* - Test getting all UTXOs for an Address. -*/ - -func TestGetUTXOsForAddress(t *testing.T) { +func TestSpendInvalidUTXO(t *testing.T) { ms, capKey, _ := SetupMultiStore() ctx := sdk.NewContext(ms, abci.Header{}, false, log.NewNopLogger()) + cdc := MakeCodec() + cdc.RegisterConcrete(testPosition{}, "x/utxo/testPosition", nil) + mapper := NewBaseMapper(capKey, cdc) + + priv, _ := ethcrypto.GenerateKey() + addr := utils.PrivKeyToAddress(priv) + + position := newTestPosition([]uint64{1, 0, 0}) + + utxo := NewUTXO(addr.Bytes(), 100, "testEther", position) + + require.True(t, utxo.Valid, "UTXO is not valid on creation") + + mapper.InvalidateUTXO(ctx, utxo) + + err := mapper.SpendUTXO(ctx, addr.Bytes(), position, [][]byte{[]byte("spenderKey")}) + + utxo = mapper.GetUTXO(ctx, addr.Bytes(), position) + require.NotNil(t, err, "Allowed invalid UTXO to be spent") + require.Nil(t, utxo.SpenderKeys, "UTXO mutated after invalid spend") + + mapper.ValidateUTXO(ctx, utxo) + err = mapper.SpendUTXO(ctx, addr.Bytes(), position, [][]byte{[]byte("spenderKey")}) + + utxo = mapper.GetUTXO(ctx, addr.Bytes(), position) + require.Nil(t, err, "Spend of valid UTXO errorred") + require.False(t, utxo.Valid, "Spent UTXO is still valid") + require.Equal(t, utxo.SpenderKeys, [][]byte{[]byte("spenderKey")}, "UTXO doesn't have spenderKeys set after valid spend") +} + +func TestUTXOMethods(t *testing.T) { + _, capKey, _ := SetupMultiStore() cdc := MakeCodec() - cdc.RegisterConcrete(&testUTXO{}, "x/utxo/testUTXO", nil) - cdc.RegisterConcrete(&testPosition{}, "x/utxo/testPosition", nil) + cdc.RegisterConcrete(testPosition{}, "x/utxo/testPosition", nil) mapper := NewBaseMapper(capKey, cdc) - privA, _ := ethcrypto.GenerateKey() - addrA := utils.PrivKeyToAddress(privA) - - privB, _ := ethcrypto.GenerateKey() - addrB := utils.PrivKeyToAddress(privB) - - privC, _ := ethcrypto.GenerateKey() - addrC := utils.PrivKeyToAddress(privC) - - positionB0 := newTestPosition([]uint64{1, 0, 0}) - positionB1 := newTestPosition([]uint64{2, 1, 0}) - positionB2 := newTestPosition([]uint64{3, 2, 1}) - - utxo0 := newTestUTXO(addrB.Bytes(), 100, positionB0) - utxo1 := newTestUTXO(addrB.Bytes(), 200, positionB1) - utxo2 := newTestUTXO(addrB.Bytes(), 300, positionB2) - - mapper.AddUTXO(ctx, utxo0) - mapper.AddUTXO(ctx, utxo1) - mapper.AddUTXO(ctx, utxo2) - - utxosForAddressB := mapper.GetUTXOsForAddress(ctx, addrB.Bytes()) - require.NotNil(t, utxosForAddressB) - require.Equal(t, 3, len(utxosForAddressB)) - require.Equal(t, utxo0, utxosForAddressB[0]) - require.Equal(t, utxo1, utxosForAddressB[1]) - require.Equal(t, utxo2, utxosForAddressB[2]) - - positionC0 := newTestPosition([]uint64{2, 3, 0}) - utxo3 := newTestUTXO(addrC.Bytes(), 300, positionC0) - mapper.AddUTXO(ctx, utxo3) - utxosForAddressC := mapper.GetUTXOsForAddress(ctx, addrC.Bytes()) - require.NotNil(t, utxosForAddressC) - require.Equal(t, 1, len(utxosForAddressC)) - require.Equal(t, utxo3, utxosForAddressC[0]) - - // check returns empty slice if no UTXOs exist for address - utxosForAddressA := mapper.GetUTXOsForAddress(ctx, addrA.Bytes()) - require.Empty(t, utxosForAddressA) + addr1 := []byte("12345") + addr2 := []byte("13579") + addr3 := []byte("67890") + + outputPos1 := testPosition{7, 8, 9} + outputPos2 := testPosition{3, 4, 5} + + outputKey1 := mapper.ConstructKey(addr1, outputPos1) + outputKey2 := mapper.ConstructKey(addr2, outputPos2) + + testUTXO := NewUTXO(addr3, 100, "Ether", testPosition{0, 1, 2}) + testUTXO.SpenderKeys = [][]byte{outputKey1, outputKey2} + + addrs := testUTXO.SpenderAddresses() + require.Equal(t, [][]byte{addr1, addr2}, addrs, "Spender addresses are not correct") + + positions := testUTXO.SpenderPositions(cdc, testProtoPosition) + require.Equal(t, []Position{&outputPos1, &outputPos2}, positions, "Spender position not correct") } diff --git a/x/utxo/types.go b/x/utxo/types.go index da69a01..f48ce20 100644 --- a/x/utxo/types.go +++ b/x/utxo/types.go @@ -2,24 +2,103 @@ package utxo import ( sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/tendermint/go-amino" ) // UTXO is a standard unspent transaction output -type UTXO interface { - // Address that owns UTXO - GetAddress() []byte - SetAddress([]byte) error // errors if already set +// When spent, it becomes invalid and spender keys are filled in +type UTXO struct { + InputKeys [][]byte // Keys in store for input UTXOs that created this output + Address []byte + Amount uint64 + Denom string + Valid bool + Position Position + TxHash []byte // transaction that created this UTXO + SpenderKeys [][]byte // Keys in store for UTXOs that spent this output +} + +func NewUTXO(owner []byte, amount uint64, denom string, position Position) UTXO { + return UTXO{ + Address: owner, + Amount: amount, + Denom: denom, + Position: position, + Valid: true, + } +} + +func NewUTXOwithInputs(owner []byte, amount uint64, denom string, position Position, txHash []byte, inputKeys [][]byte) UTXO { + return UTXO{ + InputKeys: inputKeys, + Address: owner, + Amount: amount, + Denom: denom, + Position: position, + Valid: true, + TxHash: txHash, + } +} + +func (utxo UTXO) StoreKey(cdc *amino.Codec) []byte { + encPos := cdc.MustMarshalBinaryBare(utxo.Position) + return append(utxo.Address, encPos...) +} + +// Recovers InputAddresses from Input keys. +// Assumes all addresses on-chain have same length +func (utxo UTXO) InputAddresses() [][]byte { + var addresses [][]byte + addrLen := len(utxo.Address) + for _, key := range utxo.InputKeys { + addresses = append(addresses, key[:addrLen]) + } + return addresses +} - GetAmount() uint64 - SetAmount(uint64) error // errors if already set +// Recovers InputPositions from Input keys. +// Assumes all addresses on-chain have same length +func (utxo UTXO) InputPositions(cdc *amino.Codec, proto ProtoPosition) []Position { + var inputs []Position + addrLen := len(utxo.Address) + for _, key := range utxo.InputKeys { + encodedPos := key[addrLen:] + pos := proto() + cdc.MustUnmarshalBinaryBare(encodedPos, pos) + inputs = append(inputs, pos) + } + return inputs +} - GetDenom() string - SetDenom(string) error // errors if already set +// Recovers Spender Addresses from Spender keys. +// Assumes all addresses on-chain have same length +func (utxo UTXO) SpenderAddresses() [][]byte { + var addresses [][]byte + addrLen := len(utxo.Address) + for _, key := range utxo.SpenderKeys { + addresses = append(addresses, key[:addrLen]) + } + return addresses +} - GetPosition() Position - SetPosition(Position) error // errors if already set +// Recovers Spender Positions from Spender keys. +// Assumes all addresses on-chain have same length +func (utxo UTXO) SpenderPositions(cdc *amino.Codec, proto ProtoPosition) []Position { + var spenders []Position + addrLen := len(utxo.Address) + for _, key := range utxo.SpenderKeys { + encodedPos := key[addrLen:] + pos := proto() + cdc.MustUnmarshalBinaryBare(encodedPos, pos) + spenders = append(spenders, pos) + } + return spenders } +// Create a prototype Position. +// Must return pointer to struct implementing Position +type ProtoPosition func() Position + // Positions must be unqiue or a collision may result when using mapper.go type Position interface { // Position is a uint slice diff --git a/x/utxo/utils.go b/x/utxo/utils.go index b4b2a3d..df67e15 100644 --- a/x/utxo/utils.go +++ b/x/utxo/utils.go @@ -28,5 +28,4 @@ func MakeCodec() *amino.Codec { func RegisterAmino(cdc *amino.Codec) { cdc.RegisterInterface((*Position)(nil), nil) - cdc.RegisterInterface((*UTXO)(nil), nil) }