From 0844c8c6919f6d98ebe8a9501360ef5bc362dff3 Mon Sep 17 00:00:00 2001 From: Determinant Date: Wed, 19 Aug 2020 01:25:20 -0400 Subject: catch up with the new P-Chain cross-chain impl --- go.mod | 1 + go.sum | 10 ++++ plugin/evm/atomic_tx.go | 64 -------------------- plugin/evm/base_tx.go | 42 +++---------- plugin/evm/block.go | 21 +++++++ plugin/evm/factory.go | 8 ++- plugin/evm/import_tx.go | 156 ++++++++++++++++-------------------------------- plugin/evm/service.go | 56 ++++++++++++----- plugin/evm/tx.go | 105 ++++++++++++++++++++++++++++++++ plugin/evm/user.go | 26 ++++---- plugin/evm/vm.go | 83 +++++++++++++++++++------- 11 files changed, 321 insertions(+), 251 deletions(-) delete mode 100644 plugin/evm/atomic_tx.go create mode 100644 plugin/evm/tx.go diff --git a/go.mod b/go.mod index 752be2f..5ef12a7 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/edsrzf/mmap-go v1.0.0 github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 + github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 github.com/golang/snappy v0.0.1 github.com/gorilla/rpc v1.2.0 github.com/gorilla/websocket v1.4.2 diff --git a/go.sum b/go.sum index 832a8bf..d4c1885 100644 --- a/go.sum +++ b/go.sum @@ -74,6 +74,8 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4= +github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -108,11 +110,14 @@ github.com/gorilla/rpc v1.2.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36j github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd h1:rNuUHR+CvK1IS89MMtcF0EpcVMZtjKfPRp4MEmt/aTs= github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= +github.com/hashicorp/go-plugin v1.3.0 h1:4d/wJojzvHV1I4i/rrjVaeuyxWrLzDE1mDCyDy8fXS8= github.com/hashicorp/go-plugin v1.3.0/go.mod h1:F9eH4LrE/ZsRdbwhfjs9k9HoDUwAHnYtXdgmf1AVNs0= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huin/goupnp v1.0.0 h1:wg75sLpL6DZqwHQN6E1Cfk6mtfzS45z8OV+ic+DtHRo= @@ -123,6 +128,7 @@ github.com/jackpal/gateway v1.0.6/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQ github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= +github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -153,6 +159,7 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0j github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 h1:7GoSOOW2jpsfkntVKaS2rAr1TJqfcxotyaUcuxoZSzg= github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -163,6 +170,7 @@ github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjW github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d h1:AREM5mwr4u1ORQBMvzfzBgpsctsbQikCVpvC+tX285E= github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= @@ -313,6 +321,7 @@ google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200218151345-dad8c97a84f5 h1:jB9+PJSvu5tBfmJHy/OVapFdjDF3WvpkqRhxqrmzoEU= google.golang.org/genproto v0.0.0-20200218151345-dad8c97a84f5/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -321,6 +330,7 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= diff --git a/plugin/evm/atomic_tx.go b/plugin/evm/atomic_tx.go deleted file mode 100644 index e8e48f7..0000000 --- a/plugin/evm/atomic_tx.go +++ /dev/null @@ -1,64 +0,0 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package evm - -import ( - "fmt" - - "github.com/ava-labs/gecko/database" - "github.com/ava-labs/gecko/ids" - "github.com/ava-labs/gecko/utils/crypto" - "github.com/ava-labs/gecko/utils/hashing" - "github.com/ava-labs/gecko/vms/components/verify" - "github.com/ava-labs/gecko/vms/secp256k1fx" -) - -// UnsignedAtomicTx ... -type UnsignedAtomicTx interface { - initialize(vm *VM, bytes []byte) error - ID() ids.ID - // UTXOs this tx consumes - InputUTXOs() ids.Set - // Attempts to verify this transaction with the provided state. - SemanticVerify(db database.Database, creds []verify.Verifiable) TxError - Accept(database.Batch) error -} - -// AtomicTx is an operation that can be decided without being proposed, but must -// have special control over database commitment -type AtomicTx struct { - UnsignedAtomicTx `serialize:"true"` - // Credentials that authorize the inputs to be spent - Credentials []verify.Verifiable `serialize:"true" json:"credentials"` -} - -func (vm *VM) signAtomicTx(tx *AtomicTx, signers [][]*crypto.PrivateKeySECP256K1R) error { - unsignedBytes, err := vm.codec.Marshal(tx.UnsignedAtomicTx) - if err != nil { - return fmt.Errorf("couldn't marshal UnsignedAtomicTx: %w", err) - } - - // Attach credentials - hash := hashing.ComputeHash256(unsignedBytes) - tx.Credentials = make([]verify.Verifiable, len(signers)) - for i, credKeys := range signers { - cred := &secp256k1fx.Credential{ - Sigs: make([][crypto.SECP256K1RSigLen]byte, len(credKeys)), - } - for j, key := range credKeys { - sig, err := key.SignHash(hash) // Sign hash - if err != nil { - return fmt.Errorf("problem generating credential: %w", err) - } - copy(cred.Sigs[j][:], sig) - } - tx.Credentials[i] = cred // Attach credential - } - - txBytes, err := vm.codec.Marshal(tx) - if err != nil { - return fmt.Errorf("couldn't marshal AtomicTx: %w", err) - } - return tx.initialize(vm, txBytes) -} diff --git a/plugin/evm/base_tx.go b/plugin/evm/base_tx.go index 7fd5a31..b3e85cd 100644 --- a/plugin/evm/base_tx.go +++ b/plugin/evm/base_tx.go @@ -2,10 +2,10 @@ package evm import ( "errors" - "fmt" "github.com/ava-labs/gecko/ids" - "github.com/ava-labs/gecko/vms/components/ava" + "github.com/ava-labs/gecko/snow" + "github.com/ava-labs/gecko/vms/components/avax" "github.com/ava-labs/go-ethereum/common" ) @@ -37,16 +37,9 @@ func (out *EVMOutput) Verify() error { // struct's serialized fields without doing the same on avm.BaseTx. // TODO: Factor out this and avm.BaseTX type BaseTx struct { - vm *VM + avax.Metadata // true iff this transaction has already passed syntactic verification syntacticallyVerified bool - // ID of this tx - id ids.ID - // Byte representation of this unsigned tx - unsignedBytes []byte - // Byte representation of the signed transaction (ie with credentials) - bytes []byte - // ID of the network on which this tx was issued NetworkID uint32 `serialize:"true" json:"networkID"` // ID of this blockchain. In practice is always the empty ID. @@ -55,38 +48,22 @@ type BaseTx struct { // Outputs Outs []EVMOutput `serialize:"true" json:"outputs"` // Inputs consumed by this tx - Ins []*ava.TransferableInput `serialize:"true" json:"inputs"` + Ins []*avax.TransferableInput `serialize:"true" json:"inputs"` // Memo field contains arbitrary bytes, up to maxMemoSize Memo []byte `serialize:"true" json:"memo"` } -// UnsignedBytes returns the byte representation of this unsigned tx -func (tx *BaseTx) UnsignedBytes() []byte { return tx.unsignedBytes } - -// Bytes returns the byte representation of this tx -func (tx *BaseTx) Bytes() []byte { return tx.bytes } - -// ID returns this transaction's ID -func (tx *BaseTx) ID() ids.ID { return tx.id } - // Verify returns nil iff this tx is well formed -func (tx *BaseTx) Verify() error { +func (tx *BaseTx) Verify(ctx *snow.Context) error { switch { case tx == nil: return errNilTx case tx.syntacticallyVerified: // already passed syntactic verification return nil - case tx.id.IsZero(): - return errInvalidID - case tx.vm == nil: - return errVMNil - case tx.NetworkID != tx.vm.ctx.NetworkID: + case tx.NetworkID != ctx.NetworkID: return errWrongNetworkID - case !tx.vm.ctx.ChainID.Equals(tx.BlockchainID): + case !ctx.ChainID.Equals(tx.BlockchainID): return errWrongBlockchainID - case len(tx.Memo) > maxMemoSize: - return fmt.Errorf("memo length, %d, exceeds maximum memo length, %d", - len(tx.Memo), maxMemoSize) } for _, out := range tx.Outs { if err := out.Verify(); err != nil { @@ -99,9 +76,8 @@ func (tx *BaseTx) Verify() error { } } switch { - //case !ava.IsSortedTransferableOutputs(tx.Outs, Codec): - // return errOutputsNotSorted - case !ava.IsSortedAndUniqueTransferableInputs(tx.Ins): + // TODO: check whether output addreses are sorted? + case !avax.IsSortedAndUniqueTransferableInputs(tx.Ins): return errInputsNotSortedUnique default: return nil diff --git a/plugin/evm/block.go b/plugin/evm/block.go index 449e261..9c15834 100644 --- a/plugin/evm/block.go +++ b/plugin/evm/block.go @@ -61,6 +61,27 @@ func (b *Block) Parent() snowman.Block { // Verify implements the snowman.Block interface func (b *Block) Verify() error { + p := b + path := []*Block{} + for { + if p.Status() == choices.Accepted { + break + } + path = append(path, p) + p = p.Parent().(*Block) + } + inputs := new(ids.Set) + for i := len(path) - 1; i >= 0; i-- { + p := path[i] + atx := p.vm.getAtomicTx(p.ethBlock) + inputs.Union(atx.UnsignedTx.(UnsignedAtomicTx).InputUTXOs()) + } + tx := b.vm.getAtomicTx(b.ethBlock) + atx := tx.UnsignedTx.(*UnsignedImportTx) + if atx.SemanticVerify(b.vm, tx) != nil { + return errInvalidBlock + } + _, err := b.vm.chain.InsertChain([]*types.Block{b.ethBlock}) return err } diff --git a/plugin/evm/factory.go b/plugin/evm/factory.go index 31a617a..ae2ea27 100644 --- a/plugin/evm/factory.go +++ b/plugin/evm/factory.go @@ -14,14 +14,16 @@ var ( // Factory ... type Factory struct { - AVA ids.ID - Fee uint64 + AVAX ids.ID + AVM ids.ID + Fee uint64 } // New ... func (f *Factory) New() interface{} { return &VM{ - avaxAssetID: f.AVA, + avaxAssetID: f.AVAX, + avm: f.AVM, txFee: f.Fee, } } diff --git a/plugin/evm/import_tx.go b/plugin/evm/import_tx.go index bbbdb95..84f28de 100644 --- a/plugin/evm/import_tx.go +++ b/plugin/evm/import_tx.go @@ -4,20 +4,20 @@ package evm import ( - "crypto/ecdsa" + //"crypto/ecdsa" "errors" "fmt" "github.com/ava-labs/gecko/database" "github.com/ava-labs/gecko/ids" + "github.com/ava-labs/gecko/snow" avacrypto "github.com/ava-labs/gecko/utils/crypto" - "github.com/ava-labs/gecko/utils/hashing" "github.com/ava-labs/gecko/utils/math" - "github.com/ava-labs/gecko/vms/components/ava" - "github.com/ava-labs/gecko/vms/components/verify" + "github.com/ava-labs/gecko/vms/components/avax" + //"github.com/ava-labs/gecko/vms/components/verify" "github.com/ava-labs/gecko/vms/secp256k1fx" "github.com/ava-labs/go-ethereum/common" - "github.com/ava-labs/go-ethereum/crypto" + //"github.com/ava-labs/go-ethereum/crypto" ) var ( @@ -29,29 +29,18 @@ var ( errPublicKeySignatureMismatch = errors.New("signature doesn't match public key") errUnknownAsset = errors.New("unknown asset ID") errNoFunds = errors.New("no spendable funds were found") + errWrongChainID = errors.New("tx has wrong chain ID") ) // UnsignedImportTx is an unsigned ImportTx type UnsignedImportTx struct { BaseTx `serialize:"true"` - // Inputs that consume UTXOs produced on the X-Chain - ImportedInputs []*ava.TransferableInput `serialize:"true" json:"importedInputs"` -} -// initialize [tx]. Sets [tx.vm], [tx.unsignedBytes], [tx.bytes], [tx.id] -func (tx *UnsignedImportTx) initialize(vm *VM, bytes []byte) error { - if tx.vm != nil { // already been initialized - return nil - } - tx.vm = vm - tx.bytes = bytes - tx.id = ids.NewID(hashing.ComputeHash256Array(bytes)) - var err error - tx.unsignedBytes, err = Codec.Marshal(interface{}(tx)) - if err != nil { - return fmt.Errorf("couldn't marshal UnsignedImportTx: %w", err) - } - return nil + // Which chain to consume the funds from + SourceChain ids.ID `serialize:"true" json:"sourceChain"` + + // Inputs that consume UTXOs produced on the chain + ImportedInputs []*avax.TransferableInput `serialize:"true" json:"importedInputs"` } // InputUTXOs returns the UTXOIDs of the imported funds @@ -63,66 +52,28 @@ func (tx *UnsignedImportTx) InputUTXOs() ids.Set { return set } -var ( - errInputOverflow = errors.New("inputs overflowed uint64") - errOutputOverflow = errors.New("outputs overflowed uint64") -) - -// Verify that: -// * inputs are all AVAX -// * sum(inputs{unlocked}) >= sum(outputs{unlocked}) + burnAmount{unlocked} -func syntacticVerifySpend( - ins []*ava.TransferableInput, - unlockedOuts []EVMOutput, - burnedUnlocked uint64, - avaxAssetID ids.ID, -) error { - // AVAX consumed in this tx - consumedUnlocked := uint64(0) - for _, in := range ins { - if assetID := in.AssetID(); !assetID.Equals(avaxAssetID) { // all inputs must be AVAX - return fmt.Errorf("input has unexpected asset ID %s expected %s", assetID, avaxAssetID) - } - - in := in.Input() - consumed := in.Amount() - newConsumed, err := math.Add64(consumedUnlocked, consumed) - if err != nil { - return errInputOverflow - } - consumedUnlocked = newConsumed - } - - // AVAX produced in this tx - producedUnlocked := burnedUnlocked - for _, out := range unlockedOuts { - produced := out.Amount - newProduced, err := math.Add64(producedUnlocked, produced) - if err != nil { - return errOutputOverflow - } - producedUnlocked = newProduced - } - - if producedUnlocked > consumedUnlocked { - return fmt.Errorf("tx unlocked outputs (%d) + burn amount (%d) > inputs (%d)", - producedUnlocked-burnedUnlocked, burnedUnlocked, consumedUnlocked) - } - return nil -} - // Verify this transaction is well-formed -func (tx *UnsignedImportTx) Verify() error { +func (tx *UnsignedImportTx) Verify( + avmID ids.ID, + ctx *snow.Context, + feeAmount uint64, + feeAssetID ids.ID, +) error { switch { case tx == nil: return errNilTx case tx.syntacticallyVerified: // already passed syntactic verification return nil + case tx.SourceChain.IsZero(): + return errWrongChainID + case !tx.SourceChain.Equals(avmID): + // TODO: remove this check if we allow for P->C swaps + return errWrongChainID case len(tx.ImportedInputs) == 0: return errNoImportInputs } - if err := tx.BaseTx.Verify(); err != nil { + if err := tx.BaseTx.Verify(ctx); err != nil { return err } @@ -131,27 +82,24 @@ func (tx *UnsignedImportTx) Verify() error { return err } } - if !ava.IsSortedAndUniqueTransferableInputs(tx.ImportedInputs) { + if !avax.IsSortedAndUniqueTransferableInputs(tx.ImportedInputs) { return errInputsNotSortedUnique } - allIns := make([]*ava.TransferableInput, len(tx.Ins)+len(tx.ImportedInputs)) - copy(allIns, tx.Ins) - copy(allIns[len(tx.Ins):], tx.ImportedInputs) - if err := syntacticVerifySpend(allIns, tx.Outs, tx.vm.txFee, tx.vm.avaxAssetID); err != nil { - return err - } - tx.syntacticallyVerified = true return nil } // SemanticVerify this transaction is valid. -func (tx *UnsignedImportTx) SemanticVerify(db database.Database, creds []verify.Verifiable) TxError { - if err := tx.Verify(); err != nil { +func (tx *UnsignedImportTx) SemanticVerify( + vm *VM, + stx *Tx, +) TxError { + if err := tx.Verify(vm.avm, vm.ctx, vm.txFee, vm.avaxAssetID); err != nil { return permError{err} } - // TODO: verify UTXO inputs via gRPC + // TODO: verify using avax.VerifyTx(vm.txFee, vm.avaxAssetID, tx.Ins, outs) + // TODO: verify UTXO inputs via gRPC (with creds) return nil } @@ -167,29 +115,25 @@ func (tx *UnsignedImportTx) Accept(batch database.Batch) error { // Create a new transaction func (vm *VM) newImportTx( + chainID ids.ID, // chain to import from to common.Address, // Address of recipient - keys []*ecdsa.PrivateKey, // Keys to import the funds -) (*AtomicTx, error) { + keys []*avacrypto.PrivateKeySECP256K1R, // Keys to import the funds +) (*Tx, error) { + if !vm.avm.Equals(chainID) { + return nil, errWrongChainID + } + kc := secp256k1fx.NewKeychain() - factory := &avacrypto.FactorySECP256K1R{} for _, key := range keys { - sk, err := factory.ToPrivateKey(crypto.FromECDSA(key)) - if err != nil { - panic(err) - } - kc.Add(sk.(*avacrypto.PrivateKeySECP256K1R)) + kc.Add(key) } - addrSet := ids.Set{} - for _, addr := range kc.Addresses().List() { - addrSet.Add(ids.NewID(hashing.ComputeHash256Array(addr.Bytes()))) - } - atomicUTXOs, err := vm.GetAtomicUTXOs(addrSet) + atomicUTXOs, _, _, err := vm.GetAtomicUTXOs(chainID, kc.Addresses(), ids.ShortEmpty, ids.Empty, -1) if err != nil { return nil, fmt.Errorf("problem retrieving atomic UTXOs: %w", err) } - importedInputs := []*ava.TransferableInput{} + importedInputs := []*avax.TransferableInput{} signers := [][]*avacrypto.PrivateKeySECP256K1R{} importedAmount := uint64(0) @@ -202,7 +146,7 @@ func (vm *VM) newImportTx( if err != nil { continue } - input, ok := inputIntf.(ava.TransferableIn) + input, ok := inputIntf.(avax.TransferableIn) if !ok { continue } @@ -210,27 +154,28 @@ func (vm *VM) newImportTx( if err != nil { return nil, err } - importedInputs = append(importedInputs, &ava.TransferableInput{ + importedInputs = append(importedInputs, &avax.TransferableInput{ UTXOID: utxo.UTXOID, Asset: utxo.Asset, In: input, }) signers = append(signers, utxoSigners) } - ava.SortTransferableInputsWithSigners(importedInputs, signers) + avax.SortTransferableInputsWithSigners(importedInputs, signers) if importedAmount == 0 { return nil, errNoFunds // No imported UTXOs were spendable } - ins := []*ava.TransferableInput{} + ins := []*avax.TransferableInput{} outs := []EVMOutput{} if importedAmount < vm.txFee { // imported amount goes toward paying tx fee // TODO: spend EVM balance to compensate vm.txFee-importedAmount } else if importedAmount > vm.txFee { outs = append(outs, EVMOutput{ Address: to, - Amount: importedAmount - vm.txFee}) + Amount: importedAmount - vm.txFee, + }) } // Create the transaction @@ -241,11 +186,12 @@ func (vm *VM) newImportTx( Outs: outs, Ins: ins, }, + SourceChain: chainID, ImportedInputs: importedInputs, } - tx := &AtomicTx{UnsignedAtomicTx: utx} - if err := vm.signAtomicTx(tx, signers); err != nil { + tx := &Tx{UnsignedTx: utx} + if err := tx.Sign(vm.codec, signers); err != nil { return nil, err } - return tx, utx.Verify() + return tx, utx.Verify(vm.avm, vm.ctx, vm.txFee, vm.avaxAssetID) } diff --git a/plugin/evm/service.go b/plugin/evm/service.go index 75e7c31..c05b0a4 100644 --- a/plugin/evm/service.go +++ b/plugin/evm/service.go @@ -16,6 +16,8 @@ import ( "github.com/ava-labs/coreth/core/types" "github.com/ava-labs/gecko/api" "github.com/ava-labs/gecko/utils/constants" + avacrypto "github.com/ava-labs/gecko/utils/crypto" + "github.com/ava-labs/gecko/utils/formatting" "github.com/ava-labs/go-ethereum/common" "github.com/ava-labs/go-ethereum/common/hexutil" "github.com/ava-labs/go-ethereum/crypto" @@ -147,13 +149,12 @@ func (service *AvaAPI) ExportKey(r *http.Request, args *ExportKeyArgs, reply *Ex return fmt.Errorf("problem retrieving user '%s': %w", args.Username, err) } user := user{db: db} - if address, err := service.vm.ParseAddress(args.Address); err != nil { + if address, err := service.vm.ParseLocalAddress(args.Address); err != nil { return fmt.Errorf("couldn't parse %s to address: %s", args.Address, err) } else if sk, err := user.getKey(address); err != nil { return fmt.Errorf("problem retrieving private key: %w", err) } else { - reply.PrivateKey = common.ToHex(crypto.FromECDSA(sk)) - //constants.SecretKeyPrefix + formatting.CB58{Bytes: sk.Bytes()}.String() + reply.PrivateKey = constants.SecretKeyPrefix + formatting.CB58{Bytes: sk.Bytes()}.String() return nil } } @@ -167,6 +168,10 @@ type ImportKeyArgs struct { // ImportKey adds a private key to the provided user func (service *AvaAPI) ImportKey(r *http.Request, args *ImportKeyArgs, reply *api.JsonAddress) error { service.vm.ctx.Log.Info("Platform: ImportKey called for user '%s'", args.Username) + if service.vm.ctx.Keystore == nil { + return fmt.Errorf("oh no") + } + fmt.Sprintf("good") db, err := service.vm.ctx.Keystore.GetDatabase(args.Username, args.Password) if err != nil { return fmt.Errorf("problem retrieving data: %w", err) @@ -174,35 +179,56 @@ func (service *AvaAPI) ImportKey(r *http.Request, args *ImportKeyArgs, reply *ap user := user{db: db} + factory := avacrypto.FactorySECP256K1R{} + if !strings.HasPrefix(args.PrivateKey, constants.SecretKeyPrefix) { return fmt.Errorf("private key missing %s prefix", constants.SecretKeyPrefix) } - sk, err := crypto.ToECDSA(common.FromHex(args.PrivateKey)) + trimmedPrivateKey := strings.TrimPrefix(args.PrivateKey, constants.SecretKeyPrefix) + formattedPrivateKey := formatting.CB58{} + if err := formattedPrivateKey.FromString(trimmedPrivateKey); err != nil { + return fmt.Errorf("problem parsing private key: %w", err) + } + + skIntf, err := factory.ToPrivateKey(formattedPrivateKey.Bytes) if err != nil { - return fmt.Errorf("invalid private key") + return fmt.Errorf("problem parsing private key: %w", err) } - if err = user.putAddress(sk); err != nil { + sk := skIntf.(*avacrypto.PrivateKeySECP256K1R) + + if err := user.putAddress(sk); err != nil { return fmt.Errorf("problem saving key %w", err) } - reply.Address, err = service.vm.FormatAddress(crypto.PubkeyToAddress(sk.PublicKey)) + // TODO: return eth address here + reply.Address, err = service.vm.FormatAddress( + crypto.PubkeyToAddress(*(sk.PublicKey().(*avacrypto.PublicKeySECP256K1R).ToECDSA()))) if err != nil { return fmt.Errorf("problem formatting address: %w", err) } return nil } -// ImportAVAArgs are the arguments to ImportAVA -type ImportAVAArgs struct { +// ImportAVAXArgs are the arguments to ImportAVAX +type ImportAVAXArgs struct { api.UserPass + + // Chain the funds are coming from + SourceChain string `json:"sourceChain"` + // The address that will receive the imported funds To string `json:"to"` } -// ImportAVA returns an unsigned transaction to import AVA from the X-Chain. -// The AVA must have already been exported from the X-Chain. -func (service *AvaAPI) ImportAVA(_ *http.Request, args *ImportAVAArgs, response *api.JsonTxID) error { - service.vm.ctx.Log.Info("Platform: ImportAVA called") +// ImportAVAX issues a transaction to import AVAX from the X-chain. The AVAX +// must have already been exported from the X-Chain. +func (service *AvaAPI) ImportAVAX(_ *http.Request, args *ImportAVAXArgs, response *api.JsonTxID) error { + service.vm.ctx.Log.Info("Platform: ImportAVAX called") + + chainID, err := service.vm.ctx.BCLookup.Lookup(args.SourceChain) + if err != nil { + return fmt.Errorf("problem parsing chainID %q: %w", args.SourceChain, err) + } // Get the user's info db, err := service.vm.ctx.Keystore.GetDatabase(args.Username, args.Password) @@ -211,7 +237,7 @@ func (service *AvaAPI) ImportAVA(_ *http.Request, args *ImportAVAArgs, response } user := user{db: db} - to, err := service.vm.ParseAddress(args.To) + to, err := service.vm.ParseLocalAddress(args.To) if err != nil { // Parse address return fmt.Errorf("couldn't parse argument 'to' to an address: %w", err) } @@ -221,7 +247,7 @@ func (service *AvaAPI) ImportAVA(_ *http.Request, args *ImportAVAArgs, response return fmt.Errorf("couldn't get keys controlled by the user: %w", err) } - tx, err := service.vm.newImportTx(to, privKeys) + tx, err := service.vm.newImportTx(chainID, to, privKeys) if err != nil { return err } diff --git a/plugin/evm/tx.go b/plugin/evm/tx.go new file mode 100644 index 0000000..024e54d --- /dev/null +++ b/plugin/evm/tx.go @@ -0,0 +1,105 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package evm + +import ( + "fmt" + + "github.com/ava-labs/gecko/database" + "github.com/ava-labs/gecko/database/versiondb" + "github.com/ava-labs/gecko/ids" + "github.com/ava-labs/gecko/snow" + "github.com/ava-labs/gecko/utils/codec" + "github.com/ava-labs/gecko/utils/crypto" + "github.com/ava-labs/gecko/utils/hashing" + "github.com/ava-labs/gecko/vms/components/verify" + "github.com/ava-labs/gecko/vms/secp256k1fx" +) + +// UnsignedTx is an unsigned transaction +type UnsignedTx interface { + Initialize(unsignedBytes, signedBytes []byte) + ID() ids.ID + UnsignedBytes() []byte + Bytes() []byte +} + +// UnsignedDecisionTx is an unsigned operation that can be immediately decided +type UnsignedDecisionTx interface { + UnsignedTx + + // Attempts to verify this transaction with the provided state. + SemanticVerify(vm *VM, db database.Database, stx *Tx) ( + onAcceptFunc func() error, + err TxError, + ) +} + +// UnsignedProposalTx is an unsigned operation that can be proposed +type UnsignedProposalTx interface { + UnsignedTx + + // Attempts to verify this transaction with the provided state. + SemanticVerify(vm *VM, db database.Database, stx *Tx) ( + onCommitDB *versiondb.Database, + onAbortDB *versiondb.Database, + onCommitFunc func() error, + onAbortFunc func() error, + err TxError, + ) + InitiallyPrefersCommit(vm *VM) bool +} + +// UnsignedAtomicTx is an unsigned operation that can be atomically accepted +type UnsignedAtomicTx interface { + UnsignedTx + + // UTXOs this tx consumes + InputUTXOs() ids.Set + // Attempts to verify this transaction with the provided state. + SemanticVerify(vm *VM, db database.Database, stx *Tx) TxError + + // Accept this transaction with the additionally provided state transitions. + Accept(ctx *snow.Context, batch database.Batch) error +} + +// Tx is a signed transaction +type Tx struct { + // The body of this transaction + UnsignedTx `serialize:"true" json:"unsignedTx"` + + // The credentials of this transaction + Creds []verify.Verifiable `serialize:"true" json:"credentials"` +} + +// Sign this transaction with the provided signers +func (tx *Tx) Sign(c codec.Codec, signers [][]*crypto.PrivateKeySECP256K1R) error { + unsignedBytes, err := c.Marshal(&tx.UnsignedTx) + if err != nil { + return fmt.Errorf("couldn't marshal UnsignedTx: %w", err) + } + + // Attach credentials + hash := hashing.ComputeHash256(unsignedBytes) + for _, keys := range signers { + cred := &secp256k1fx.Credential{ + Sigs: make([][crypto.SECP256K1RSigLen]byte, len(keys)), + } + for i, key := range keys { + sig, err := key.SignHash(hash) // Sign hash + if err != nil { + return fmt.Errorf("problem generating credential: %w", err) + } + copy(cred.Sigs[i][:], sig) + } + tx.Creds = append(tx.Creds, cred) // Attach credential + } + + signedBytes, err := c.Marshal(tx) + if err != nil { + return fmt.Errorf("couldn't marshal ProposalTx: %w", err) + } + tx.Initialize(unsignedBytes, signedBytes) + return nil +} diff --git a/plugin/evm/user.go b/plugin/evm/user.go index 651f202..5d7a037 100644 --- a/plugin/evm/user.go +++ b/plugin/evm/user.go @@ -5,12 +5,13 @@ package evm import ( "errors" + "fmt" - "crypto/ecdsa" "github.com/ava-labs/gecko/database" "github.com/ava-labs/gecko/ids" + "github.com/ava-labs/gecko/utils/crypto" "github.com/ava-labs/go-ethereum/common" - "github.com/ava-labs/go-ethereum/crypto" + ethcrypto "github.com/ava-labs/go-ethereum/crypto" ) // Key in the database whose corresponding value is the list of @@ -66,12 +67,13 @@ func (u *user) controlsAddress(address common.Address) (bool, error) { } // putAddress persists that this user controls address controlled by [privKey] -func (u *user) putAddress(privKey *ecdsa.PrivateKey) error { +func (u *user) putAddress(privKey *crypto.PrivateKeySECP256K1R) error { if privKey == nil { return errKeyNil } - address := crypto.PubkeyToAddress(privKey.PublicKey) // address the privKey controls + address := ethcrypto.PubkeyToAddress( + (*privKey.PublicKey().(*crypto.PublicKeySECP256K1R).ToECDSA())) // address the privKey controls controlsAddress, err := u.controlsAddress(address) if err != nil { return err @@ -80,7 +82,7 @@ func (u *user) putAddress(privKey *ecdsa.PrivateKey) error { return nil } - if err := u.db.Put(address.Bytes(), crypto.FromECDSA(privKey)); err != nil { // Address --> private key + if err := u.db.Put(address.Bytes(), privKey.Bytes()); err != nil { // Address --> private key return err } @@ -106,31 +108,35 @@ func (u *user) putAddress(privKey *ecdsa.PrivateKey) error { } // Key returns the private key that controls the given address -func (u *user) getKey(address common.Address) (*ecdsa.PrivateKey, error) { +func (u *user) getKey(address common.Address) (*crypto.PrivateKeySECP256K1R, error) { if u.db == nil { return nil, errDBNil //} else if address.IsZero() { // return nil, errEmptyAddress } + factory := crypto.FactorySECP256K1R{} bytes, err := u.db.Get(address.Bytes()) if err != nil { return nil, err } - sk, err := crypto.ToECDSA(bytes) + sk, err := factory.ToPrivateKey(bytes) if err != nil { return nil, err } - return sk, nil + if sk, ok := sk.(*crypto.PrivateKeySECP256K1R); ok { + return sk, nil + } + return nil, fmt.Errorf("expected private key to be type *crypto.PrivateKeySECP256K1R but is type %T", sk) } // Return all private keys controlled by this user -func (u *user) getKeys() ([]*ecdsa.PrivateKey, error) { +func (u *user) getKeys() ([]*crypto.PrivateKeySECP256K1R, error) { addrs, err := u.getAddresses() if err != nil { return nil, err } - keys := make([]*ecdsa.PrivateKey, len(addrs)) + keys := make([]*crypto.PrivateKeySECP256K1R, len(addrs)) for i, addr := range addrs { key, err := u.getKey(addr) if err != nil { diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index 1010d60..4fe4cc0 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -16,6 +16,7 @@ import ( "github.com/ava-labs/coreth" "github.com/ava-labs/coreth/core" + "github.com/ava-labs/coreth/core/state" "github.com/ava-labs/coreth/core/types" "github.com/ava-labs/coreth/eth" "github.com/ava-labs/coreth/node" @@ -33,10 +34,12 @@ import ( "github.com/ava-labs/gecko/snow/choices" "github.com/ava-labs/gecko/snow/consensus/snowman" "github.com/ava-labs/gecko/utils/codec" + //"github.com/ava-labs/gecko/utils/constants" + //"github.com/ava-labs/gecko/utils/formatting" avajson "github.com/ava-labs/gecko/utils/json" "github.com/ava-labs/gecko/utils/timer" "github.com/ava-labs/gecko/utils/wrappers" - "github.com/ava-labs/gecko/vms/components/ava" + "github.com/ava-labs/gecko/vms/components/avax" commonEng "github.com/ava-labs/gecko/snow/engine/common" ) @@ -69,13 +72,14 @@ const ( ) var ( - errEmptyBlock = errors.New("empty block") - errCreateBlock = errors.New("couldn't create block") - errUnknownBlock = errors.New("unknown block") - errBlockFrequency = errors.New("too frequent block issuance") - errUnsupportedFXs = errors.New("unsupported feature extensions") - errInvalidBlock = errors.New("invalid block") - errInvalidAddr = errors.New("invalid hex address") + errEmptyBlock = errors.New("empty block") + errCreateBlock = errors.New("couldn't create block") + errUnknownBlock = errors.New("unknown block") + errBlockFrequency = errors.New("too frequent block issuance") + errUnsupportedFXs = errors.New("unsupported feature extensions") + errInvalidBlock = errors.New("invalid block") + errInvalidAddr = errors.New("invalid hex address") + errTooManyAtomicTx = errors.New("too many pending atomix txs") ) func maxDuration(x, y time.Duration) time.Duration { @@ -128,13 +132,25 @@ type VM struct { bdGenWaitFlag bool bdGenFlag bool - genlock sync.Mutex - txSubmitChan <-chan struct{} - codec codec.Codec - clock timer.Clock - avaxAssetID ids.ID - txFee uint64 - //atomicTxPool [] + genlock sync.Mutex + txSubmitChan <-chan struct{} + codec codec.Codec + clock timer.Clock + avaxAssetID ids.ID + avm ids.ID + txFee uint64 + pendingAtomicTxs chan *Tx + blockAtomicInputCache cache.LRU +} + +func (vm *VM) getAtomicTx(block *types.Block) *Tx { + var atx *Tx + if extdata := block.ExtraData(); extdata != nil { + if err := vm.codec.Unmarshal(block.ExtraData(), atx); err != nil { + panic(err) + } + } + return atx } /* @@ -191,6 +207,11 @@ func (vm *VM) Initialize( vm.newBlockChan <- nil return errEmptyBlock } + select { + case atx := <-vm.pendingAtomicTxs: + raw, _ := vm.codec.Marshal(atx) + block.SetExtraData(raw) + } return nil }) chain.SetOnSealFinish(func(block *types.Block) error { @@ -211,8 +232,14 @@ func (vm *VM) Initialize( chain.SetOnQueryAcceptedBlock(func() *types.Block { return vm.getLastAccepted().ethBlock }) + chain.SetOnExtraStateChange(func(block *types.Block, statedb *state.StateDB) error { + atx := vm.getAtomicTx(block).UnsignedTx.(*UnsignedImportTx) + vm.ctx.Log.Info(atx.ID().String()) + return nil + }) vm.blockCache = cache.LRU{Size: 2048} vm.blockStatusCache = cache.LRU{Size: 1024} + vm.blockAtomicInputCache = cache.LRU{Size: 4096} vm.newBlockChan = make(chan *Block) vm.networkChan = toEngine vm.blockDelayTimer = timer.NewTimer(func() { @@ -236,6 +263,8 @@ func (vm *VM) Initialize( vm.bdGenWaitFlag = true vm.newTxPoolHeadChan = make(chan core.NewTxPoolHeadEvent, 1) vm.txPoolStabilizedOk = make(chan struct{}, 1) + // TODO: read size from options + vm.pendingAtomicTxs = make(chan *Tx, 1024) chain.GetTxPool().SubscribeNewHeadEvent(vm.newTxPoolHeadChan) // TODO: shutdown this go routine go ctx.Log.RecoverAndPanic(func() { @@ -419,7 +448,7 @@ func (vm *VM) CreateHandlers() map[string]*commonEng.HTTPHandler { return map[string]*commonEng.HTTPHandler{ "/rpc": &commonEng.HTTPHandler{LockOptions: commonEng.NoLock, Handler: handler}, - "/ava": newHandler("", &AvaAPI{vm}), + "/ava": newHandler("ava", &AvaAPI{vm}), "/ws": &commonEng.HTTPHandler{LockOptions: commonEng.NoLock, Handler: handler.WebsocketHandler([]string{"*"})}, } } @@ -584,7 +613,8 @@ func (vm *VM) getLastAccepted() *Block { return vm.lastAccepted } -func (vm *VM) ParseAddress(addrStr string) (common.Address, error) { +// ParseLocalAddress takes in an address for this chain and produces the ID +func (vm *VM) ParseLocalAddress(addrStr string) (common.Address, error) { if !common.IsHexAddress(addrStr) { return common.Address{}, errInvalidAddr } @@ -595,14 +625,25 @@ func (vm *VM) FormatAddress(addr common.Address) (string, error) { return addr.Hex(), nil } -func (vm *VM) issueTx(tx *AtomicTx) error { +func (vm *VM) issueTx(tx *Tx) error { + select { + case vm.pendingAtomicTxs <- tx: + default: + return errTooManyAtomicTx + } return nil } // GetAtomicUTXOs returns the utxos that at least one of the provided addresses is // referenced in. -func (vm *VM) GetAtomicUTXOs(addrs ids.Set) ([]*ava.UTXO, error) { +func (vm *VM) GetAtomicUTXOs( + chainID ids.ID, + addrs ids.ShortSet, + startAddr ids.ShortID, + startUTXOID ids.ID, + limit int, +) ([]*avax.UTXO, ids.ShortID, ids.ID, error) { // TODO: finish this function via gRPC - utxos := []*ava.UTXO{} - return utxos, nil + utxos := []*avax.UTXO{} + return utxos, ids.ShortEmpty, ids.Empty, nil } -- cgit v1.2.3