aboutsummaryrefslogtreecommitdiff
path: root/plugin/evm
diff options
context:
space:
mode:
Diffstat (limited to 'plugin/evm')
-rw-r--r--plugin/evm/export_tx.go25
-rw-r--r--plugin/evm/export_tx_test.go132
-rw-r--r--plugin/evm/import_tx.go31
-rw-r--r--plugin/evm/import_tx_test.go349
-rw-r--r--plugin/evm/service.go11
-rw-r--r--plugin/evm/tx.go70
-rw-r--r--plugin/evm/vm.go61
-rw-r--r--plugin/evm/vm_test.go133
8 files changed, 761 insertions, 51 deletions
diff --git a/plugin/evm/export_tx.go b/plugin/evm/export_tx.go
index f210352..d099eb2 100644
--- a/plugin/evm/export_tx.go
+++ b/plugin/evm/export_tx.go
@@ -92,16 +92,25 @@ func (tx *UnsignedExportTx) SemanticVerify(
return permError{err}
}
+ if len(tx.Ins) != len(stx.Creds) {
+ return permError{errSignatureInputsMismatch}
+ }
+
f := crypto.FactorySECP256K1R{}
- for i, cred := range stx.Creds {
+ for i, input := range tx.Ins {
+ cred := stx.Creds[i].(*secp256k1fx.Credential)
if err := cred.Verify(); err != nil {
return permError{err}
}
- pubKey, err := f.RecoverPublicKey(tx.UnsignedBytes(), cred.(*secp256k1fx.Credential).Sigs[0][:])
+
+ if len(cred.Sigs) != 1 {
+ return permError{fmt.Errorf("expected one signature for EVM Input Credential, but found: %d", len(cred.Sigs))}
+ }
+ pubKey, err := f.RecoverPublicKey(tx.UnsignedBytes(), cred.Sigs[0][:])
if err != nil {
return permError{err}
}
- if tx.Ins[i].Address != PublicKeyToEthAddress(pubKey) {
+ if input.Address != PublicKeyToEthAddress(pubKey) {
return permError{errPublicKeySignatureMismatch}
}
}
@@ -160,7 +169,7 @@ func (tx *UnsignedExportTx) Accept(ctx *snow.Context, _ database.Batch) error {
return ctx.SharedMemory.Put(tx.DestinationChain, elems)
}
-// Create a new transaction
+// newExportTx returns a new ExportTx
func (vm *VM) newExportTx(
assetID ids.ID, // AssetID of the tokens to export
amount uint64, // Amount of tokens to export
@@ -183,14 +192,14 @@ func (vm *VM) newExportTx(
toBurn = vm.txFee
}
// burn AVAX
- ins, signers, err := vm.GetSpendableCanonical(keys, vm.ctx.AVAXAssetID, toBurn)
+ ins, signers, err := vm.GetSpendableFunds(keys, vm.ctx.AVAXAssetID, toBurn)
if err != nil {
return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err)
}
// burn non-AVAX
if !assetID.Equals(vm.ctx.AVAXAssetID) {
- ins2, signers2, err := vm.GetSpendableCanonical(keys, assetID, amount)
+ ins2, signers2, err := vm.GetSpendableFunds(keys, assetID, amount)
if err != nil {
return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err)
}
@@ -210,6 +219,9 @@ func (vm *VM) newExportTx(
},
}}
+ avax.SortTransferableOutputs(exportOuts, vm.codec)
+ SortEVMInputsAndSigners(ins, signers)
+
// Create the transaction
utx := &UnsignedExportTx{
NetworkID: vm.ctx.NetworkID,
@@ -225,6 +237,7 @@ func (vm *VM) newExportTx(
return tx, utx.Verify(vm.ctx.XChainID, vm.ctx, vm.txFee, vm.ctx.AVAXAssetID)
}
+// EVMStateTransfer executes the state update from the atomic export transaction
func (tx *UnsignedExportTx) EVMStateTransfer(vm *VM, state *state.StateDB) error {
addrs := map[[20]byte]uint64{}
for _, from := range tx.Ins {
diff --git a/plugin/evm/export_tx_test.go b/plugin/evm/export_tx_test.go
new file mode 100644
index 0000000..319c6dd
--- /dev/null
+++ b/plugin/evm/export_tx_test.go
@@ -0,0 +1,132 @@
+// (c) 2019-2020, Ava Labs, Inc. All rights reserved.
+// See the file LICENSE for licensing terms.
+
+package evm
+
+import (
+ "testing"
+
+ "github.com/ava-labs/avalanchego/ids"
+ "github.com/ava-labs/avalanchego/utils/crypto"
+ "github.com/ava-labs/avalanchego/vms/components/avax"
+ "github.com/ava-labs/avalanchego/vms/secp256k1fx"
+)
+
+func TestExportTxVerifyNil(t *testing.T) {
+ var exportTx *UnsignedExportTx
+ if err := exportTx.Verify(testXChainID, NewContext(), testTxFee, testAvaxAssetID); err == nil {
+ t.Fatal("Verify should have failed due to nil transaction")
+ }
+}
+
+func TestExportTxVerify(t *testing.T) {
+ var exportAmount uint64 = 10000000
+ exportTx := &UnsignedExportTx{
+ NetworkID: testNetworkID,
+ BlockchainID: testCChainID,
+ DestinationChain: testXChainID,
+ Ins: []EVMInput{
+ {
+ Address: testEthAddrs[0],
+ Amount: exportAmount,
+ AssetID: testAvaxAssetID,
+ Nonce: 0,
+ },
+ {
+ Address: testEthAddrs[2],
+ Amount: exportAmount,
+ AssetID: testAvaxAssetID,
+ Nonce: 0,
+ },
+ },
+ ExportedOutputs: []*avax.TransferableOutput{
+ {
+ Asset: avax.Asset{ID: testAvaxAssetID},
+ Out: &secp256k1fx.TransferOutput{
+ Amt: exportAmount - testTxFee,
+ OutputOwners: secp256k1fx.OutputOwners{
+ Locktime: 0,
+ Threshold: 1,
+ Addrs: []ids.ShortID{testShortIDAddrs[0]},
+ },
+ },
+ },
+ {
+ Asset: avax.Asset{ID: testAvaxAssetID},
+ Out: &secp256k1fx.TransferOutput{
+ Amt: exportAmount, // only subtract fee from one output
+ OutputOwners: secp256k1fx.OutputOwners{
+ Locktime: 0,
+ Threshold: 1,
+ Addrs: []ids.ShortID{testShortIDAddrs[1]},
+ },
+ },
+ },
+ },
+ }
+
+ // Sort the inputs and outputs to ensure the transaction is canonical
+ avax.SortTransferableOutputs(exportTx.ExportedOutputs, Codec)
+ // Pass in a list of signers here with the appropriate length
+ // to avoid causing a nil-pointer error in the helper method
+ emptySigners := make([][]*crypto.PrivateKeySECP256K1R, 2)
+ SortEVMInputsAndSigners(exportTx.Ins, emptySigners)
+
+ ctx := NewContext()
+
+ // Test Valid Export Tx
+ if err := exportTx.Verify(testXChainID, ctx, testTxFee, testAvaxAssetID); err != nil {
+ t.Fatalf("Failed to verify valid ExportTx: %w", err)
+ }
+
+ exportTx.syntacticallyVerified = false
+ exportTx.NetworkID = testNetworkID + 1
+
+ // Test Incorrect Network ID Errors
+ if err := exportTx.Verify(testXChainID, ctx, testTxFee, testAvaxAssetID); err == nil {
+ t.Fatal("ExportTx should have failed verification due to incorrect network ID")
+ }
+
+ exportTx.syntacticallyVerified = false
+ exportTx.NetworkID = testNetworkID
+ exportTx.BlockchainID = nonExistentID
+ // Test Incorrect Blockchain ID Errors
+ if err := exportTx.Verify(testXChainID, ctx, testTxFee, testAvaxAssetID); err == nil {
+ t.Fatal("ExportTx should have failed verification due to incorrect blockchain ID")
+ }
+
+ exportTx.syntacticallyVerified = false
+ exportTx.BlockchainID = testCChainID
+ exportTx.DestinationChain = nonExistentID
+ // Test Incorrect Destination Chain ID Errors
+ if err := exportTx.Verify(testXChainID, ctx, testTxFee, testAvaxAssetID); err == nil {
+ t.Fatal("ExportTx should have failed verification due to incorrect destination chain")
+ }
+
+ exportTx.syntacticallyVerified = false
+ exportTx.DestinationChain = testXChainID
+ exportedOuts := exportTx.ExportedOutputs
+ exportTx.ExportedOutputs = nil
+ // Test No Exported Outputs Errors
+ if err := exportTx.Verify(testXChainID, ctx, testTxFee, testAvaxAssetID); err == nil {
+ t.Fatal("ExportTx should have failed verification due to no exported outputs")
+ }
+
+ exportTx.syntacticallyVerified = false
+ exportTx.ExportedOutputs = []*avax.TransferableOutput{exportedOuts[1], exportedOuts[0]}
+ // Test Unsorted outputs Errors
+ if err := exportTx.Verify(testXChainID, ctx, testTxFee, testAvaxAssetID); err == nil {
+ t.Fatal("ExportTx should have failed verification due to no unsorted exported outputs")
+ }
+
+ exportTx.syntacticallyVerified = false
+ exportTx.ExportedOutputs = []*avax.TransferableOutput{exportedOuts[0], nil}
+ // Test invalid exported output
+ if err := exportTx.Verify(testXChainID, ctx, testTxFee, testAvaxAssetID); err == nil {
+ t.Fatal("ExportTx should have failed verification due to invalid output")
+ }
+}
+
+func TestExportTxSemanticVerify(t *testing.T) {
+
+}
diff --git a/plugin/evm/import_tx.go b/plugin/evm/import_tx.go
index 0b0d348..ae6b540 100644
--- a/plugin/evm/import_tx.go
+++ b/plugin/evm/import_tx.go
@@ -98,6 +98,10 @@ func (tx *UnsignedImportTx) SemanticVerify(
return permError{err}
}
+ if len(stx.Creds) != len(tx.ImportedInputs) {
+ return permError{errSignatureInputsMismatch}
+ }
+
// do flow-checking
fc := avax.NewFlowChecker()
//fc.Produce(vm.ctx.AVAXAssetID, vm.txFee)
@@ -122,20 +126,12 @@ func (tx *UnsignedImportTx) SemanticVerify(
for i, in := range tx.ImportedInputs {
utxoIDs[i] = in.UTXOID.InputID().Bytes()
}
+ // allUTXOBytes is guaranteed to be the same length as utxoIDs
allUTXOBytes, err := vm.ctx.SharedMemory.Get(tx.SourceChain, utxoIDs)
if err != nil {
return tempError{err}
}
- utxos := make([]*avax.UTXO, len(tx.ImportedInputs))
- for i, utxoBytes := range allUTXOBytes {
- utxo := &avax.UTXO{}
- if err := vm.codec.Unmarshal(utxoBytes, utxo); err != nil {
- return tempError{err}
- }
- utxos[i] = utxo
- }
-
for i, in := range tx.ImportedInputs {
utxoBytes := allUTXOBytes[i]
@@ -173,7 +169,7 @@ func (tx *UnsignedImportTx) Accept(ctx *snow.Context, _ database.Batch) error {
return ctx.SharedMemory.Remove(tx.SourceChain, utxoIDs)
}
-// Create a new transaction
+// newImportTx returns a new ImportTx
func (vm *VM) newImportTx(
chainID ids.ID, // chain to import from
to common.Address, // Address of recipient
@@ -240,20 +236,23 @@ func (vm *VM) newImportTx(
// })
//}
- // non-AVAX asset outputs
- for aidKey, amount := range importedAmount {
- aid := ids.NewID(aidKey)
- //if aid.Equals(vm.ctx.AVAXAssetID) || amount == 0 {
+ // This will create unique outputs (in the context of sorting)
+ // since each output will have a unique assetID
+ for assetKey, amount := range importedAmount {
+ assetID := ids.NewID(assetKey)
+ //if assetID.Equals(vm.ctx.AVAXAssetID) || amount == 0 {
if amount == 0 {
continue
}
outs = append(outs, EVMOutput{
Address: to,
Amount: amount,
- AssetID: aid,
+ AssetID: assetID,
})
}
+ SortEVMOutputs(outs)
+
// Create the transaction
utx := &UnsignedImportTx{
NetworkID: vm.ctx.NetworkID,
@@ -269,6 +268,8 @@ func (vm *VM) newImportTx(
return tx, utx.Verify(vm.ctx.XChainID, vm.ctx, vm.txFee, vm.ctx.AVAXAssetID)
}
+// EVMStateTransfer performs the state transfer to increase the balances of
+// accounts accordingly with the imported EVMOutputs
func (tx *UnsignedImportTx) EVMStateTransfer(vm *VM, state *state.StateDB) error {
for _, to := range tx.Outs {
log.Info("crosschain X->C", "addr", to.Address, "amount", to.Amount)
diff --git a/plugin/evm/import_tx_test.go b/plugin/evm/import_tx_test.go
new file mode 100644
index 0000000..fcd18ac
--- /dev/null
+++ b/plugin/evm/import_tx_test.go
@@ -0,0 +1,349 @@
+// (c) 2019-2020, Ava Labs, Inc. All rights reserved.
+// See the file LICENSE for licensing terms.
+
+package evm
+
+import (
+ "testing"
+
+ "github.com/ava-labs/avalanchego/chains/atomic"
+ "github.com/ava-labs/avalanchego/ids"
+ "github.com/ava-labs/avalanchego/utils/crypto"
+ "github.com/ava-labs/avalanchego/vms/components/avax"
+ "github.com/ava-labs/avalanchego/vms/secp256k1fx"
+)
+
+func TestImportTxVerifyNil(t *testing.T) {
+ var importTx *UnsignedImportTx
+ if err := importTx.Verify(testXChainID, NewContext(), testTxFee, testAvaxAssetID); err == nil {
+ t.Fatal("Verify should have failed due to nil transaction")
+ }
+}
+
+func TestImportTxVerify(t *testing.T) {
+ var importAmount uint64 = 10000000
+ txID := ids.NewID([32]byte{0xff})
+ importTx := &UnsignedImportTx{
+ NetworkID: testNetworkID,
+ BlockchainID: testCChainID,
+ SourceChain: testXChainID,
+ ImportedInputs: []*avax.TransferableInput{
+ {
+ UTXOID: avax.UTXOID{
+ TxID: txID,
+ OutputIndex: uint32(0),
+ },
+ Asset: avax.Asset{ID: testAvaxAssetID},
+ In: &secp256k1fx.TransferInput{
+ Amt: importAmount,
+ Input: secp256k1fx.Input{
+ SigIndices: []uint32{0},
+ },
+ },
+ },
+ {
+ UTXOID: avax.UTXOID{
+ TxID: txID,
+ OutputIndex: uint32(1),
+ },
+ Asset: avax.Asset{ID: testAvaxAssetID},
+ In: &secp256k1fx.TransferInput{
+ Amt: importAmount,
+ Input: secp256k1fx.Input{
+ SigIndices: []uint32{0},
+ },
+ },
+ },
+ },
+ Outs: []EVMOutput{
+ {
+ Address: testEthAddrs[0],
+ Amount: importAmount,
+ AssetID: testAvaxAssetID,
+ },
+ {
+ Address: testEthAddrs[1],
+ Amount: importAmount,
+ AssetID: testAvaxAssetID,
+ },
+ },
+ }
+
+ ctx := NewContext()
+
+ // // Sort the inputs and outputs to ensure the transaction is canonical
+ avax.SortTransferableInputs(importTx.ImportedInputs)
+ SortEVMOutputs(importTx.Outs)
+
+ // Test Valid ImportTx
+ if err := importTx.Verify(testXChainID, ctx, testTxFee, testAvaxAssetID); err != nil {
+ t.Fatalf("Failed to verify ImportTx: %w", err)
+ }
+
+ importTx.syntacticallyVerified = false
+ importTx.NetworkID = testNetworkID + 1
+
+ // // Test Incorrect Network ID Errors
+ if err := importTx.Verify(testXChainID, ctx, testTxFee, testAvaxAssetID); err == nil {
+ t.Fatal("ImportTx should have failed verification due to incorrect network ID")
+ }
+
+ importTx.syntacticallyVerified = false
+ importTx.NetworkID = testNetworkID
+ importTx.BlockchainID = nonExistentID
+ // // Test Incorrect Blockchain ID Errors
+ if err := importTx.Verify(testXChainID, ctx, testTxFee, testAvaxAssetID); err == nil {
+ t.Fatal("ImportTx should have failed verification due to incorrect blockchain ID")
+ }
+
+ importTx.syntacticallyVerified = false
+ importTx.BlockchainID = testCChainID
+ importTx.SourceChain = nonExistentID
+ // // Test Incorrect Destination Chain ID Errors
+ if err := importTx.Verify(testXChainID, ctx, testTxFee, testAvaxAssetID); err == nil {
+ t.Fatal("ImportTx should have failed verification due to incorrect source chain")
+ }
+
+ importTx.syntacticallyVerified = false
+ importTx.SourceChain = testXChainID
+ importedIns := importTx.ImportedInputs
+ importTx.ImportedInputs = nil
+ // // Test No Exported Outputs Errors
+ if err := importTx.Verify(testXChainID, ctx, testTxFee, testAvaxAssetID); err == nil {
+ t.Fatal("ImportTx should have failed verification due to no imported inputs")
+ }
+
+ importTx.syntacticallyVerified = false
+ importTx.ImportedInputs = []*avax.TransferableInput{importedIns[1], importedIns[0]}
+ // // Test Unsorted Imported Inputs Errors
+ if err := importTx.Verify(testXChainID, ctx, testTxFee, testAvaxAssetID); err == nil {
+ t.Fatal("ImportTx should have failed verification due to unsorted import inputs")
+ }
+
+ importTx.syntacticallyVerified = false
+ importTx.ImportedInputs = []*avax.TransferableInput{importedIns[0], nil}
+ if err := importTx.Verify(testXChainID, ctx, testTxFee, testAvaxAssetID); err == nil {
+ t.Fatal("ImportTx should have failed verification due to invalid input")
+ }
+}
+
+func TestImportTxSemanticVerify(t *testing.T) {
+ _, vm, _, sharedMemory := GenesisVM(t, false)
+
+ xChainSharedMemory := sharedMemory.NewSharedMemory(vm.ctx.XChainID)
+
+ importAmount := uint64(1000000)
+ utxoID := avax.UTXOID{
+ TxID: ids.NewID([32]byte{
+ 0x0f, 0x2f, 0x4f, 0x6f, 0x8e, 0xae, 0xce, 0xee,
+ 0x0d, 0x2d, 0x4d, 0x6d, 0x8c, 0xac, 0xcc, 0xec,
+ 0x0b, 0x2b, 0x4b, 0x6b, 0x8a, 0xaa, 0xca, 0xea,
+ 0x09, 0x29, 0x49, 0x69, 0x88, 0xa8, 0xc8, 0xe8,
+ }),
+ }
+
+ utxo := &avax.UTXO{
+ UTXOID: utxoID,
+ Asset: avax.Asset{ID: vm.ctx.AVAXAssetID},
+ Out: &secp256k1fx.TransferOutput{
+ Amt: importAmount,
+ OutputOwners: secp256k1fx.OutputOwners{
+ Threshold: 1,
+ Addrs: []ids.ShortID{testKeys[0].PublicKey().Address()},
+ },
+ },
+ }
+ utxoBytes, err := vm.codec.Marshal(utxo)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ evmOutput := EVMOutput{
+ Address: testEthAddrs[0],
+ Amount: importAmount,
+ AssetID: vm.ctx.AVAXAssetID,
+ }
+ unsignedImportTx := &UnsignedImportTx{
+ NetworkID: vm.ctx.NetworkID,
+ BlockchainID: vm.ctx.ChainID,
+ SourceChain: vm.ctx.XChainID,
+ ImportedInputs: []*avax.TransferableInput{{
+ UTXOID: utxoID,
+ Asset: avax.Asset{ID: vm.ctx.AVAXAssetID},
+ In: &secp256k1fx.TransferInput{
+ Amt: importAmount,
+ Input: secp256k1fx.Input{SigIndices: []uint32{0}},
+ },
+ }},
+ Outs: []EVMOutput{evmOutput},
+ }
+
+ state, err := vm.chain.BlockState(vm.lastAccepted.ethBlock)
+ if err != nil {
+ t.Fatalf("Failed to get last accepted stateDB due to: %s", err)
+ }
+
+ if empty := state.Empty(testEthAddrs[0]); !empty {
+ t.Fatalf("Expected ethereum address to have empty starting balance.")
+ }
+
+ if err := unsignedImportTx.Verify(vm.ctx.XChainID, vm.ctx, vm.txFee, vm.ctx.AVAXAssetID); err != nil {
+ t.Fatal(err)
+ }
+ unsignedImportTx.syntacticallyVerified = false
+
+ tx := &Tx{UnsignedTx: unsignedImportTx}
+
+ // Sign with the correct key
+ if err := tx.Sign(vm.codec, [][]*crypto.PrivateKeySECP256K1R{{testKeys[0]}}); err != nil {
+ t.Fatal(err)
+ }
+
+ // Check that SemanticVerify passes without the UTXO being present during bootstrapping
+ if err := unsignedImportTx.SemanticVerify(vm, tx); err != nil {
+ t.Fatal("Should have failed to import non-existent UTXO")
+ }
+
+ if err := xChainSharedMemory.Put(vm.ctx.ChainID, []*atomic.Element{{
+ Key: utxo.InputID().Bytes(),
+ Value: utxoBytes,
+ Traits: [][]byte{
+ testKeys[0].PublicKey().Address().Bytes(),
+ },
+ }}); err != nil {
+ t.Fatal(err)
+ }
+
+ // Check that SemanticVerify passes when the UTXO is present during bootstrapping
+ if err := unsignedImportTx.SemanticVerify(vm, tx); err != nil {
+ t.Fatalf("Semantic verification should have passed during bootstrapping when the UTXO was present")
+ }
+
+ // Check that SemanticVerify does not pass if an additional output is added in
+ unsignedImportTx.Outs = append(unsignedImportTx.Outs, EVMOutput{
+ Address: testEthAddrs[1],
+ Amount: importAmount,
+ AssetID: vm.ctx.AVAXAssetID,
+ })
+
+ if err := unsignedImportTx.SemanticVerify(vm, tx); err == nil {
+ t.Fatal("Semantic verification should have failed due to insufficient funds")
+ }
+
+ unsignedImportTx.Outs = []EVMOutput{evmOutput}
+
+ if err := vm.Bootstrapping(); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := vm.Bootstrapped(); err != nil {
+ t.Fatal(err)
+ }
+
+ vm.ctx.Bootstrapped()
+
+ // Remove the signature
+ tx.Creds = nil
+ if err := unsignedImportTx.SemanticVerify(vm, tx); err == nil {
+ t.Fatalf("SemanticVerify should have failed due to no signatures")
+ }
+
+ // Sign with the incorrect key
+ if err := tx.Sign(vm.codec, [][]*crypto.PrivateKeySECP256K1R{{testKeys[1]}}); err != nil {
+ t.Fatal(err)
+ }
+ if err := unsignedImportTx.SemanticVerify(vm, tx); err == nil {
+ t.Fatalf("SemanticVerify should have failed due to an invalid signature")
+ }
+
+ // Re-sign with the correct key
+ tx.Creds = nil
+ if err := tx.Sign(vm.codec, [][]*crypto.PrivateKeySECP256K1R{{testKeys[0]}}); err != nil {
+ t.Fatal(err)
+ }
+
+ // Check that SemanticVerify passes when the UTXO is present after bootstrapping
+ if err := unsignedImportTx.SemanticVerify(vm, tx); err != nil {
+ t.Fatalf("Semantic verification should have passed after bootstrapping with the UTXO present")
+ }
+
+ if err := unsignedImportTx.Accept(vm.ctx, nil); err != nil {
+ t.Fatalf("Accept failed due to: %w", err)
+ }
+
+ if err := unsignedImportTx.EVMStateTransfer(vm, state); err != nil {
+ t.Fatalf("EVM State Transfer failed due to: %s", err)
+ }
+
+ balance := state.GetBalance(testEthAddrs[0])
+ if balance == nil {
+ t.Fatal("Found nil balance for address receiving imported funds")
+ } else if balance.Uint64() != importAmount*x2cRate.Uint64() {
+ t.Fatalf("Balance was %d, but expected balance of: %d", balance.Uint64(), importAmount*x2cRate.Uint64())
+ }
+
+ // Check that SemanticVerify fails when the UTXO is not present after bootstrapping
+ if err := unsignedImportTx.SemanticVerify(vm, tx); err == nil {
+ t.Fatalf("Semantic verification should have failed after the UTXO removed from shared memory")
+ }
+}
+
+func TestNewImportTx(t *testing.T) {
+ _, vm, _, sharedMemory := GenesisVM(t, true)
+
+ importAmount := uint64(1000000)
+ utxoID := avax.UTXOID{
+ TxID: ids.NewID([32]byte{
+ 0x0f, 0x2f, 0x4f, 0x6f, 0x8e, 0xae, 0xce, 0xee,
+ 0x0d, 0x2d, 0x4d, 0x6d, 0x8c, 0xac, 0xcc, 0xec,
+ 0x0b, 0x2b, 0x4b, 0x6b, 0x8a, 0xaa, 0xca, 0xea,
+ 0x09, 0x29, 0x49, 0x69, 0x88, 0xa8, 0xc8, 0xe8,
+ }),
+ }
+
+ utxo := &avax.UTXO{
+ UTXOID: utxoID,
+ Asset: avax.Asset{ID: vm.ctx.AVAXAssetID},
+ Out: &secp256k1fx.TransferOutput{
+ Amt: importAmount,
+ OutputOwners: secp256k1fx.OutputOwners{
+ Threshold: 1,
+ Addrs: []ids.ShortID{testKeys[0].PublicKey().Address()},
+ },
+ },
+ }
+ utxoBytes, err := vm.codec.Marshal(utxo)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ xChainSharedMemory := sharedMemory.NewSharedMemory(vm.ctx.XChainID)
+
+ if err := xChainSharedMemory.Put(vm.ctx.ChainID, []*atomic.Element{{
+ Key: utxo.InputID().Bytes(),
+ Value: utxoBytes,
+ Traits: [][]byte{
+ testKeys[0].PublicKey().Address().Bytes(),
+ },
+ }}); err != nil {
+ t.Fatal(err)
+ }
+
+ tx, err := vm.newImportTx(vm.ctx.XChainID, testEthAddrs[0], []*crypto.PrivateKeySECP256K1R{testKeys[0]})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ importTx, ok := tx.UnsignedTx.(UnsignedAtomicTx)
+ if !ok {
+ t.Fatal("newImportTx did not return an atomic transaction")
+ }
+
+ if err := importTx.SemanticVerify(vm, tx); err != nil {
+ t.Fatalf("newImportTx created an invalid transaction")
+ }
+
+ if err := importTx.Accept(vm.ctx, nil); err != nil {
+ t.Fatalf("Failed to accept import transaction due to: %s", err)
+ }
+}
diff --git a/plugin/evm/service.go b/plugin/evm/service.go
index 8ede1b0..a844f10 100644
--- a/plugin/evm/service.go
+++ b/plugin/evm/service.go
@@ -151,7 +151,7 @@ type ExportKeyReply struct {
func (service *AvaxAPI) ExportKey(r *http.Request, args *ExportKeyArgs, reply *ExportKeyReply) error {
log.Info("EVM: ExportKey called")
- address, err := service.vm.ParseEthAddress(args.Address)
+ address, err := ParseEthAddress(args.Address)
if err != nil {
return fmt.Errorf("couldn't parse %s to address: %s", args.Address, err)
}
@@ -200,10 +200,7 @@ func (service *AvaxAPI) ImportKey(r *http.Request, args *ImportKeyArgs, reply *a
sk := skIntf.(*crypto.PrivateKeySECP256K1R)
// TODO: return eth address here
- reply.Address, err = service.vm.FormatEthAddress(GetEthAddress(sk))
- if err != nil {
- return fmt.Errorf("problem formatting address: %w", err)
- }
+ reply.Address = FormatEthAddress(GetEthAddress(sk))
db, err := service.vm.ctx.Keystore.GetDatabase(args.Username, args.Password)
if err != nil {
@@ -234,7 +231,7 @@ func (service *AvaxAPI) ImportAVAX(_ *http.Request, args *ImportArgs, response *
return service.Import(nil, args, response)
}
-// ImportAVAX issues a transaction to import AVAX from the X-chain. The AVAX
+// Import issues a transaction to import AVAX from the X-chain. The AVAX
// must have already been exported from the X-Chain.
func (service *AvaxAPI) Import(_ *http.Request, args *ImportArgs, response *api.JsonTxID) error {
log.Info("EVM: ImportAVAX called")
@@ -244,7 +241,7 @@ func (service *AvaxAPI) Import(_ *http.Request, args *ImportArgs, response *api.
return fmt.Errorf("problem parsing chainID %q: %w", args.SourceChain, err)
}
- to, err := service.vm.ParseEthAddress(args.To)
+ to, err := ParseEthAddress(args.To)
if err != nil { // Parse address
return fmt.Errorf("couldn't parse argument 'to' to an address: %w", err)
}
diff --git a/plugin/evm/tx.go b/plugin/evm/tx.go
index 9580bc0..7c2ebf1 100644
--- a/plugin/evm/tx.go
+++ b/plugin/evm/tx.go
@@ -4,14 +4,17 @@
package evm
import (
+ "bytes"
"errors"
"fmt"
+ "sort"
"github.com/ava-labs/coreth/core/state"
"github.com/ava-labs/avalanchego/database"
"github.com/ava-labs/avalanchego/ids"
"github.com/ava-labs/avalanchego/snow"
+ "github.com/ava-labs/avalanchego/utils"
"github.com/ava-labs/avalanchego/utils/codec"
"github.com/ava-labs/avalanchego/utils/crypto"
"github.com/ava-labs/avalanchego/utils/hashing"
@@ -30,12 +33,14 @@ var (
errNilTx = errors.New("tx is nil")
)
+// EVMOutput defines an output from EVM State created from export transactions
type EVMOutput struct {
Address common.Address `serialize:"true" json:"address"`
Amount uint64 `serialize:"true" json:"amount"`
AssetID ids.ID `serialize:"true" json:"assetID"`
}
+// EVMInput defines an input for the EVM State to be used in import transactions
type EVMInput struct {
Address common.Address `serialize:"true" json:"address"`
Amount uint64 `serialize:"true" json:"amount"`
@@ -43,10 +48,12 @@ type EVMInput struct {
Nonce uint64 `serialize:"true" json:"nonce"`
}
+// Verify ...
func (out *EVMOutput) Verify() error {
return nil
}
+// Verify ...
func (in *EVMInput) Verify() error {
return nil
}
@@ -115,3 +122,66 @@ func (tx *Tx) Sign(c codec.Codec, signers [][]*crypto.PrivateKeySECP256K1R) erro
tx.Initialize(unsignedBytes, signedBytes)
return nil
}
+
+// innerSortInputsAndSigners implements sort.Interface for EVMInput
+type innerSortInputsAndSigners struct {
+ inputs []EVMInput
+ signers [][]*crypto.PrivateKeySECP256K1R
+}
+
+func (ins *innerSortInputsAndSigners) Less(i, j int) bool {
+ addrComp := bytes.Compare(ins.inputs[i].Address.Bytes(), ins.inputs[j].Address.Bytes())
+ if addrComp != 0 {
+ return addrComp < 0
+ }
+ return bytes.Compare(ins.inputs[i].AssetID.Bytes(), ins.inputs[j].AssetID.Bytes()) < 0
+}
+
+func (ins *innerSortInputsAndSigners) Len() int { return len(ins.inputs) }
+
+func (ins *innerSortInputsAndSigners) Swap(i, j int) {
+ ins.inputs[j], ins.inputs[i] = ins.inputs[i], ins.inputs[j]
+ ins.signers[j], ins.signers[i] = ins.signers[i], ins.signers[j]
+}
+
+// SortEVMInputsAndSigners sorts the list of EVMInputs based on the addresses and assetIDs
+func SortEVMInputsAndSigners(inputs []EVMInput, signers [][]*crypto.PrivateKeySECP256K1R) {
+ sort.Sort(&innerSortInputsAndSigners{inputs: inputs, signers: signers})
+}
+
+// IsSortedAndUniqueEVMInputs returns true if the EVM Inputs are sorted and unique
+// based on the account addresses
+func IsSortedAndUniqueEVMInputs(inputs []EVMInput) bool {
+ return utils.IsSortedAndUnique(&innerSortInputsAndSigners{inputs: inputs})
+}
+
+// innerSortEVMOutputs implements sort.Interface for EVMOutput
+type innerSortEVMOutputs struct {
+ outputs []EVMOutput
+}
+
+func (outs *innerSortEVMOutputs) Less(i, j int) bool {
+ addrComp := bytes.Compare(outs.outputs[i].Address.Bytes(), outs.outputs[j].Address.Bytes())
+ if addrComp != 0 {
+ return addrComp < 0
+ }
+ return bytes.Compare(outs.outputs[i].AssetID.Bytes(), outs.outputs[j].AssetID.Bytes()) < 0
+}
+
+func (outs *innerSortEVMOutputs) Len() int { return len(outs.outputs) }
+
+func (outs *innerSortEVMOutputs) Swap(i, j int) {
+ outs.outputs[j], outs.outputs[i] = outs.outputs[i], outs.outputs[j]
+}
+
+// SortEVMOutputs sorts the list of EVMOutputs based on the addresses and assetIDs
+// of the outputs
+func SortEVMOutputs(outputs []EVMOutput) {
+ sort.Sort(&innerSortEVMOutputs{outputs: outputs})
+}
+
+// IsSortedAndUniqueEVMOutputs returns true if the EVMOutputs are sorted and unique
+// based on the account addresses and assetIDs
+func IsSortedAndUniqueEVMOutputs(outputs []EVMOutput) bool {
+ return utils.IsSortedAndUnique(&innerSortEVMOutputs{outputs: outputs})
+}
diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go
index 9ad7411..f19c105 100644
--- a/plugin/evm/vm.go
+++ b/plugin/evm/vm.go
@@ -103,12 +103,16 @@ var (
errNoImportInputs = errors.New("tx has no imported inputs")
errInputsNotSortedUnique = errors.New("inputs not sorted and unique")
errPublicKeySignatureMismatch = errors.New("signature doesn't match public key")
+ errSignatureInputsMismatch = errors.New("number of inputs does not match number of signatures")
errUnknownAsset = errors.New("unknown asset ID")
errNoFunds = errors.New("no spendable funds were found")
errWrongChainID = errors.New("tx has wrong chain ID")
errInsufficientFunds = errors.New("insufficient funds")
- errNoExportOutputs = errors.New("no export outputs")
- errOutputsNotSorted = errors.New("outputs not sorted")
+ errNoExportOutputs = errors.New("tx has no export outputs")
+ errOutputsNotSorted = errors.New("tx outputs not sorted")
+ errNoImportOutputs = errors.New("tx has no outputs to import")
+ errNoExportInputs = errors.New("tx has no inputs to export")
+ errInputsNotSortedAndUnique = errors.New("inputs not sorted and unique")
errOverflowExport = errors.New("overflow when computing export amount + txFee")
errInvalidNonce = errors.New("invalid nonce")
)
@@ -772,17 +776,6 @@ func (vm *VM) getLastAccepted() *Block {
return vm.lastAccepted
}
-func (vm *VM) ParseEthAddress(addrStr string) (common.Address, error) {
- if !common.IsHexAddress(addrStr) {
- return common.Address{}, errInvalidAddr
- }
- return common.HexToAddress(addrStr), nil
-}
-
-func (vm *VM) FormatEthAddress(addr common.Address) (string, error) {
- return addr.Hex(), nil
-}
-
// ParseAddress takes in an address and produces the ID of the chain it's for
// the ID of the address
func (vm *VM) ParseAddress(addrStr string) (ids.ID, ids.ShortID, error) {
@@ -871,16 +864,11 @@ func (vm *VM) GetAtomicUTXOs(
return utxos, lastAddrID, lastUTXOID, nil
}
-func GetEthAddress(privKey *crypto.PrivateKeySECP256K1R) common.Address {
- return PublicKeyToEthAddress(privKey.PublicKey())
-}
-
-func PublicKeyToEthAddress(pubKey crypto.PublicKey) common.Address {
- return ethcrypto.PubkeyToAddress(
- (*pubKey.(*crypto.PublicKeySECP256K1R).ToECDSA()))
-}
-
-func (vm *VM) GetSpendableCanonical(keys []*crypto.PrivateKeySECP256K1R, assetID ids.ID, amount uint64) ([]EVMInput, [][]*crypto.PrivateKeySECP256K1R, error) {
+// GetSpendableFunds returns a list of EVMInputs and keys (in corresponding order)
+// to total [amount] of [assetID] owned by [keys]
+// TODO switch to returning a list of private keys
+// since there are no multisig inputs in Ethereum
+func (vm *VM) GetSpendableFunds(keys []*crypto.PrivateKeySECP256K1R, assetID ids.ID, amount uint64) ([]EVMInput, [][]*crypto.PrivateKeySECP256K1R, error) {
// NOTE: should we use HEAD block or lastAccepted?
state, err := vm.chain.BlockState(vm.lastAccepted.ethBlock)
if err != nil {
@@ -920,12 +908,15 @@ func (vm *VM) GetSpendableCanonical(keys []*crypto.PrivateKeySECP256K1R, assetID
signers = append(signers, []*crypto.PrivateKeySECP256K1R{key})
amount -= balance
}
+
if amount > 0 {
return nil, nil, errInsufficientFunds
}
+
return inputs, signers, nil
}
+// GetAcceptedNonce returns the nonce associated with the address at the last accepted block
func (vm *VM) GetAcceptedNonce(address common.Address) (uint64, error) {
state, err := vm.chain.BlockState(vm.lastAccepted.ethBlock)
if err != nil {
@@ -933,3 +924,27 @@ func (vm *VM) GetAcceptedNonce(address common.Address) (uint64, error) {
}
return state.GetNonce(address), nil
}
+
+// ParseEthAddress parses [addrStr] and returns an Ethereum address
+func ParseEthAddress(addrStr string) (common.Address, error) {
+ if !common.IsHexAddress(addrStr) {
+ return common.Address{}, errInvalidAddr
+ }
+ return common.HexToAddress(addrStr), nil
+}
+
+// FormatEthAddress formats [addr] into a string
+func FormatEthAddress(addr common.Address) string {
+ return addr.Hex()
+}
+
+// GetEthAddress returns the ethereum address derived from [privKey]
+func GetEthAddress(privKey *crypto.PrivateKeySECP256K1R) common.Address {
+ return PublicKeyToEthAddress(privKey.PublicKey())
+}
+
+// PublicKeyToEthAddress returns the ethereum address derived from [pubKey]
+func PublicKeyToEthAddress(pubKey crypto.PublicKey) common.Address {
+ return ethcrypto.PubkeyToAddress(
+ (*pubKey.(*crypto.PublicKeySECP256K1R).ToECDSA()))
+}
diff --git a/plugin/evm/vm_test.go b/plugin/evm/vm_test.go
new file mode 100644
index 0000000..8ce7825
--- /dev/null
+++ b/plugin/evm/vm_test.go
@@ -0,0 +1,133 @@
+// (c) 2019-2020, Ava Labs, Inc. All rights reserved.
+// See the file LICENSE for licensing terms.
+
+package evm
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/ava-labs/avalanchego/api/keystore"
+ "github.com/ava-labs/avalanchego/chains/atomic"
+ "github.com/ava-labs/avalanchego/database/memdb"
+ "github.com/ava-labs/avalanchego/database/prefixdb"
+ "github.com/ava-labs/avalanchego/ids"
+ "github.com/ava-labs/avalanchego/snow"
+ engCommon "github.com/ava-labs/avalanchego/snow/engine/common"
+ "github.com/ava-labs/avalanchego/utils/crypto"
+ "github.com/ava-labs/avalanchego/utils/formatting"
+ "github.com/ava-labs/avalanchego/utils/logging"
+ "github.com/ava-labs/coreth/core"
+ "github.com/ethereum/go-ethereum/common"
+)
+
+var (
+ testNetworkID uint32 = 10
+ testCChainID = ids.NewID([32]byte{'c', 'c', 'h', 'a', 'i', 'n', 't', 'e', 's', 't'})
+ testXChainID = ids.NewID([32]byte{'t', 'e', 's', 't', 'x'})
+ nonExistentID = ids.NewID([32]byte{'F'})
+ testTxFee = uint64(1000)
+ startBalance = uint64(50000)
+ testKeys []*crypto.PrivateKeySECP256K1R
+ testEthAddrs []common.Address // testEthAddrs[i] corresponds to testKeys[i]
+ testShortIDAddrs []ids.ShortID
+ testAvaxAssetID = ids.NewID([32]byte{1, 2, 3})
+ username = "Johns"
+ password = "CjasdjhiPeirbSenfeI13" // #nosec G101
+ ethChainID uint32 = 43112
+)
+
+func init() {
+ cb58 := formatting.CB58{}
+ factory := crypto.FactorySECP256K1R{}
+
+ for _, key := range []string{
+ "24jUJ9vZexUM6expyMcT48LBx27k1m7xpraoV62oSQAHdziao5",
+ "2MMvUMsxx6zsHSNXJdFD8yc5XkancvwyKPwpw4xUK3TCGDuNBY",
+ "cxb7KpGWhDMALTjNNSJ7UQkkomPesyWAPUaWRGdyeBNzR6f35",
+ } {
+ _ = cb58.FromString(key)
+ pk, _ := factory.ToPrivateKey(cb58.Bytes)
+ secpKey := pk.(*crypto.PrivateKeySECP256K1R)
+ testKeys = append(testKeys, secpKey)
+ testEthAddrs = append(testEthAddrs, GetEthAddress(secpKey))
+ testShortIDAddrs = append(testShortIDAddrs, pk.PublicKey().Address())
+ }
+}
+
+// BuildGenesisTest returns the genesis bytes for Coreth VM to be used in testing
+func BuildGenesisTest(t *testing.T) []byte {
+ ss := StaticService{}
+
+ genesisJSON := "{\"config\":{\"chainId\":43112,\"homesteadBlock\":0,\"daoForkBlock\":0,\"daoForkSupport\":true,\"eip150Block\":0,\"eip150Hash\":\"0x2086799aeebeae135c246c65021c82b4e15a2c451340993aacfd2751886514f0\",\"eip155Block\":0,\"eip158Block\":0,\"byzantiumBlock\":0,\"constantinopleBlock\":0,\"petersburgBlock\":0,\"istanbulBlock\":0,\"muirGlacierBlock\":0},\"nonce\":\"0x0\",\"timestamp\":\"0x0\",\"extraData\":\"0x00\",\"gasLimit\":\"0x5f5e100\",\"difficulty\":\"0x0\",\"mixHash\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"coinbase\":\"0x0000000000000000000000000000000000000000\",\"alloc\":{\"0100000000000000000000000000000000000000\":{\"code\":\"0x7300000000000000000000000000000000000000003014608060405260043610603d5760003560e01c80631e010439146042578063b6510bb314606e575b600080fd5b605c60048036036020811015605657600080fd5b503560b1565b60408051918252519081900360200190f35b818015607957600080fd5b5060af60048036036080811015608e57600080fd5b506001600160a01b03813516906020810135906040810135906060013560b6565b005b30cd90565b836001600160a01b031681836108fc8690811502906040516000604051808303818888878c8acf9550505050505015801560f4573d6000803e3d6000fd5b505050505056fea26469706673582212201eebce970fe3f5cb96bf8ac6ba5f5c133fc2908ae3dcd51082cfee8f583429d064736f6c634300060a0033\",\"balance\":\"0x0\"}},\"number\":\"0x0\",\"gasUsed\":\"0x0\",\"parentHash\":\"0x0000000000000000000000000000000000000000000000000000000000000000\"}"
+
+ genesis := &core.Genesis{}
+ if err := json.Unmarshal([]byte(genesisJSON), genesis); err != nil {
+ t.Fatalf("Problem unmarshaling genesis JSON: %w", err)
+ }
+ genesisReply, err := ss.BuildGenesis(nil, genesis)
+ if err != nil {
+ t.Fatalf("Failed to create test genesis")
+ }
+ return genesisReply.Bytes
+}
+
+func NewContext() *snow.Context {
+ ctx := snow.DefaultContextTest()
+ ctx.NetworkID = testNetworkID
+ ctx.ChainID = testCChainID
+ ctx.AVAXAssetID = testAvaxAssetID
+ ctx.XChainID = ids.Empty.Prefix(0)
+ aliaser := ctx.BCLookup.(*ids.Aliaser)
+ aliaser.Alias(testCChainID, "C")
+ aliaser.Alias(testCChainID, testCChainID.String())
+ aliaser.Alias(testXChainID, "X")
+ aliaser.Alias(testXChainID, testXChainID.String())
+
+ // SNLookup might be required here???
+ return ctx
+}
+
+// GenesisVM creates a VM instance with the genesis test bytes and returns
+// the channel use to send messages to the engine, the vm, and atomic memory
+func GenesisVM(t *testing.T, finishBootstrapping bool) (chan engCommon.Message, *VM, []byte, *atomic.Memory) {
+ genesisBytes := BuildGenesisTest(t)
+ ctx := NewContext()
+
+ baseDB := memdb.New()
+
+ m := &atomic.Memory{}
+ m.Initialize(logging.NoLog{}, prefixdb.New([]byte{0}, baseDB))
+ ctx.SharedMemory = m.NewSharedMemory(ctx.ChainID)
+
+ // NB: this lock is intentionally left locked when this function returns.
+ // The caller of this function is responsible for unlocking.
+ ctx.Lock.Lock()
+
+ userKeystore := keystore.CreateTestKeystore()
+ if err := userKeystore.AddUser(username, password); err != nil {
+ t.Fatal(err)
+ }
+ ctx.Keystore = userKeystore.NewBlockchainKeyStore(ctx.ChainID)
+
+ issuer := make(chan engCommon.Message, 1)
+ vm := &VM{
+ txFee: testTxFee,
+ }
+ err := vm.Initialize(
+ ctx,
+ prefixdb.New([]byte{1}, baseDB),
+ genesisBytes,
+ issuer,
+ []*engCommon.Fx{},
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ return issuer, vm, genesisBytes, m
+}
+
+func TestVMGenesis(t *testing.T) {
+ _, _, _, _ = GenesisVM(t, true)
+}
89'>1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233