-
Notifications
You must be signed in to change notification settings - Fork 16.8k
Expand file tree
/
Copy pathethereum.go
More file actions
266 lines (246 loc) · 10.7 KB
/
Copy pathethereum.go
File metadata and controls
266 lines (246 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
package fsm
import (
"bytes"
"fmt"
"github.com/alecthomas/units"
"github.com/canopy-network/canopy/lib"
"github.com/canopy-network/canopy/lib/crypto"
ethTypes "github.com/ethereum/go-ethereum/core/types"
"math/big"
"time"
)
/* This file implements an Ethereum translation layer over Canopy transactions in order to allow popular tooling to interact with Canopy */
// Flow
// 1. An Ethereum wallet creates the transaction
// 2. Canopy translates the RLP to a standard Canopy transaction and gossips it like normal
// 3. If RLP detected during tx processing, verify the signature and generate a new transaction from the RLP to verify equality
// CNPYContractAddress: (CNPY) a fake contract address that allows tools to send/receive CNPY as if it's an ERC20
const CNPYContractAddress = `0x0000000000000000000000000000000000000001`
const SendSelector = "a9059cbb" // transfer(address,uint256) # Canopy expects valid ABI encoding to be 100% compatible with send tooling
const SubsidySelector = "16d68b09" // while the signature is subsidy(bytes), Canopy expects (selector + proto-bytes)
// StakedCNPYContractAddress: (stCNPY) a fake contract address that allows tools to stake, edit, and unstake CNPY as a delegator
const StakedCNPYContractAddress = `0x0000000000000000000000000000000000000002`
const StakeSelector = "2d1e0c02" // while the signature is stake(bytes), Canopy expects (selector + proto-bytes)
const EditStakeSelector = "8c71a515" // while the signature is editStake(bytes), Canopy expects (selector + proto-bytes)
const UnstakeSelector = "3c3653e2" // while the signature is unstake(bytes), Canopy expects (selector + proto-bytes)
// SwapCNPYContractAddress: (swCNPY) a fake contract address that allows tools to create a sell order, edit a sell order, and delete a sell order
const SwapCNPYContractAddress = `0x0000000000000000000000000000000000000003`
const CreateOrderSelector = "bc2e8e5f" // while the signature is createOrder(bytes), Canopy expects (selector + proto-bytes)
const EditOrderSelector = "74e78d6f" // while the signature is editOrder(bytes), Canopy expects (selector + proto-bytes)
const DeleteOrderSelector = "6c4650e7" // while the signature is deleteOrder(bytes), Canopy expects (selector + proto-bytes)
// RLPIndicator is a human-readable indicator the tx is translated from RLP
const RLPIndicator = "RLP"
// RLPToCanopyTransaction() converts an RLP encoded transaction into a Canopy transaction
func RLPToCanopyTransaction(txBytes []byte) (transaction *lib.Transaction, e lib.ErrorI) {
// protect against spam
if len(txBytes) > int(2*units.KB) {
return nil, ErrInvalidRLPTx(fmt.Errorf("max transaction size"))
}
// decode transaction to ethereum object
var tx ethTypes.Transaction
if err := tx.UnmarshalBinary(txBytes); err != nil {
return nil, ErrInvalidRLPTx(err)
}
// get the signer type (supports: Legacy, EIP-155, EIP-1559, EIP-2930, EIP-4844, EIP-7702)
signer := ethTypes.LatestSignerForChainID(tx.ChainId())
// recover the public key from the rlp transaction and validate the signature
publicKey, err := crypto.RecoverPublicKey(signer, tx)
if err != nil {
return nil, ErrInvalidPublicKey(err)
}
// ensure the EVM chain id fits into Canopy's uint64 translation.
if tx.ChainId() == nil || !tx.ChainId().IsUint64() {
return nil, ErrInvalidRLPTx(fmt.Errorf("chain id exceeds uint64"))
}
// extract chain id and network id from the evm chain id
chainId, networkId := EvmChainIdToCanopyIds(tx.ChainId().Uint64())
// compute fee with unsigned gas to avoid signed narrowing skew
fee, ok := DownscaleTo6DecimalsChecked(new(big.Int).Mul(new(big.Int).SetUint64(tx.Gas()), tx.GasPrice()))
if !ok {
return nil, ErrInvalidRLPTx(fmt.Errorf("invalid fee amount"))
}
// generate the transaction object
transaction = &lib.Transaction{
MessageType: MessageSendName, // fallback default
Signature: &lib.Signature{
PublicKey: publicKey.Bytes(),
Signature: txBytes, // store the raw transaction here
},
CreatedHeight: tx.Nonce(), // the rpc ensures a proper value that satisfies replay protection
Time: pseudoEthereumTimestamp(tx.Gas()),
Fee: fee,
NetworkId: networkId,
Memo: RLPIndicator,
ChainId: chainId,
}
// extract a message from the rlp transaction
msg, e := rlpToMessage(publicKey, transaction, tx)
// handle any error
if e != nil {
return
}
// convert the message to an `any`
transaction.Msg, e = lib.NewAny(msg)
// exit
return
}
// rlpToMessage() converts an ethereum RLP transaction to a message
func rlpToMessage(publicKey crypto.PublicKeyI, transaction *lib.Transaction, tx ethTypes.Transaction) (msg lib.MessageI, e lib.ErrorI) {
// get the relevant tx fields
to, from, data := tx.To(), publicKey.Address().Bytes(), tx.Data()
// ensure non-nil to
if to == nil {
return nil, ErrRecipientAddressEmpty()
}
// switch on the 'recipient'
switch tx.To().Hex() {
// if the recipient is a pseudo-contract call
case CNPYContractAddress, StakedCNPYContractAddress, SwapCNPYContractAddress:
// ensure enough data for a selector
if len(data) < 4 {
return nil, ErrInvalidERC20Tx(fmt.Errorf("data too short"))
}
// switch on the selector
switch selector := lib.BytesToString(data[:4]); selector {
case SendSelector:
msg, e = ethDataToMsgSend(from, data)
case StakeSelector:
m := new(MessageStake)
msg, e = ethDataToMsg(MessageStakeName, transaction, m, data, func() {
// allow the omission of the public key because it may be difficult to get the public key from the wallet
if len(m.PublicKey) == 0 {
m.PublicKey = publicKey.Bytes()
}
})
case EditStakeSelector:
m := new(MessageEditStake)
msg, e = ethDataToMsg(MessageEditStakeName, transaction, m, data, nil)
case UnstakeSelector:
m := new(MessageUnstake)
msg, e = ethDataToMsg(MessageUnstakeName, transaction, m, data, nil)
case CreateOrderSelector:
m := new(MessageCreateOrder)
msg, e = ethDataToMsg(MessageCreateOrderName, transaction, m, data, nil)
case EditOrderSelector:
msg, e = ethDataToMsg(MessageEditOrderName, transaction, new(MessageEditOrder), data, nil)
case DeleteOrderSelector:
msg, e = ethDataToMsg(MessageDeleteOrderName, transaction, new(MessageDeleteOrder), data, nil)
case SubsidySelector:
m := new(MessageSubsidy)
msg, e = ethDataToMsg(MessageSubsidyName, transaction, m, data, nil)
default:
e = ErrInvalidERC20Tx(fmt.Errorf("unsupported selector: 0x%s", selector))
}
default: // non-contract call (transfer() only)
amount, ok := DownscaleTo6DecimalsChecked(tx.Value())
if !ok || amount == 0 {
return nil, ErrInvalidAmount()
}
msg = &MessageSend{
FromAddress: from,
ToAddress: tx.To().Bytes(),
Amount: amount,
}
}
return
}
// ethDataToMsgSend() translates the 'data' from an RLP transaction into a MessageSend
// - not protobuf, actual ABI encoding to allow native ERC20 transfer() calls
func ethDataToMsgSend(fromAddress, data []byte) (*MessageSend, lib.ErrorI) {
// check input length (4 selector + 32 address + 32 amount = 136 hex chars + 2 for "0x")
if len(data) < 4+32+32 {
return nil, ErrInvalidERC20Tx(fmt.Errorf("input too short"))
}
// amount: full 32-byte uint256
amount := new(big.Int).SetBytes(data[36:68])
// sanity check the amount
if !amount.IsUint64() || amount.Uint64() == 0 {
return nil, ErrInvalidAmount()
}
// return the message send type
return &MessageSend{
ToAddress: data[16:36],
FromAddress: fromAddress,
Amount: amount.Uint64(), // take amount as is - because decimals is specified at the 'contract level'
}, nil
}
// ethDataToMsg() converts the ethereum tx data (message proto bytes) to a message
func ethDataToMsg(messageType string, transaction *lib.Transaction, msg lib.MessageI, data []byte, callback func()) (lib.MessageI, lib.ErrorI) {
// set the message type
transaction.MessageType = messageType
// convert the data after the selector to a message
if err := lib.Unmarshal(data[4:], msg); err != nil {
return nil, err
}
// execute the callback
if callback != nil {
callback()
}
// sanity check the message
return msg, msg.Check()
}
// VerifyRLPBytes() implements special 'signature verification logic' that allows a MessageSend to be authenticated using a signed RLP transaction
func (s *StateMachine) VerifyRLPBytes(tx *lib.Transaction) lib.ErrorI {
// create a compare transaction from the signature field
compare, err := RLPToCanopyTransaction(tx.Signature.Signature)
if err != nil {
return err
}
// get the transaction hash (includes the raw RLP) for the compare tx
compareHash, err := compare.GetHash()
if err != nil {
return err
}
// get the transaction hash (includes the raw RLP) for the raw tx
originalHash, err := tx.GetHash()
if err != nil {
return err
}
// check the equality of the two transactions
if !bytes.Equal(compareHash, originalHash) {
return ErrInvalidSignature()
}
// exit without error
return nil
}
// ChainId translation design:
// evmChainId is High 32 bits = networkId and Low 32 bits = chainId
// - avoids conflicts with existing Ethereum chain IDs
// - merges Canopy's dual network ID scheme into a single combined identifier
// EvmChainIdToCanopyIds() converts an EVM chainId to a Canopy chain id
func EvmChainIdToCanopyIds(evmChainId uint64) (chainId, networkId uint64) {
networkId = evmChainId >> 32
chainId = evmChainId & 0xFFFFFFFF
return
}
// CanopyIdsToEVMChainId() converts a chainId and networkId to an evm chain ID
func CanopyIdsToEVMChainId(chainId, networkId uint64) uint64 {
return (networkId << 32) | (chainId & 0xFFFFFFFF)
}
// scaleFactor allows conversion from 6 decimal places to 18 10^12
var scaleFactor = big.NewInt(1_000_000_000_000)
// UpscaleTo18Decimals converts a 6-decimal unit (Canopy native) to 18-decimal (Ethereum RPC)
func UpscaleTo18Decimals(amount uint64) *big.Int {
return new(big.Int).Mul(big.NewInt(int64(amount)), scaleFactor)
}
// DownscaleTo6Decimals converts from 18-decimal unit (Ethereum RPC) to 6-decimal (Canopy native)
func DownscaleTo6Decimals(amount *big.Int) uint64 {
return new(big.Int).Div(amount, scaleFactor).Uint64()
}
// DownscaleTo6DecimalsChecked converts from 18-decimal unit (Ethereum RPC) to 6-decimal (Canopy native)
// and reports overflow/invalid input instead of wrapping through Uint64().
func DownscaleTo6DecimalsChecked(amount *big.Int) (uint64, bool) {
if amount == nil || amount.Sign() < 0 {
return 0, false
}
downscaled := new(big.Int).Div(amount, scaleFactor)
if !downscaled.IsUint64() {
return 0, false
}
return downscaled.Uint64(), true
}
// pseudoEthereumTimestamp() creates a fake timestamp to ensure collision resistance using the gas limit variable
// - note: the gas-limit may be used to extend the pseudo-nonce functionality if needed
func pseudoEthereumTimestamp(gasLimit uint64) uint64 {
return uint64(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC).Add(time.Duration(gasLimit) * time.Second).Unix())
}