From 97a01d992848f00b054d658429565774f192ea7d Mon Sep 17 00:00:00 2001 From: Samuel Manzanera Date: Fri, 10 Jan 2025 15:27:55 +0100 Subject: [PATCH 1/2] lowercase custom error message --- utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils.go b/utils.go index ec9a9eb..bbf93d6 100644 --- a/utils.go +++ b/utils.go @@ -75,7 +75,7 @@ func ParseBigInt(number string, formatDecimals uint) (*big.Int, error) { r := regexp.MustCompile(`^([0-9]*)\.?([0-9]*)$`) matches := r.FindAllStringSubmatch(number, -1) if len(matches) == 0 || len(matches[0]) == 0 || len(matches[0][1])+len(matches[0][2]) == 0 { - return nil, errors.New("Invalid number") + return nil, errors.New("invalid number") } wholeStr := matches[0][1] From c81f09a9a956c92b05f4ccb6fc4e2406623cb3af Mon Sep 17 00:00:00 2001 From: Samuel Manzanera Date: Fri, 10 Jan 2025 15:37:51 +0100 Subject: [PATCH 2/2] Add WASM contract interaction --- contract.go | 140 ++++++++++++++++++++++++++++++++++++ go.mod | 7 +- go.sum | 2 + transaction_builder.go | 42 ++++++++--- transaction_builder_test.go | 105 +++++++++++++++++---------- 5 files changed, 247 insertions(+), 49 deletions(-) create mode 100644 contract.go diff --git a/contract.go b/contract.go new file mode 100644 index 0000000..e30a498 --- /dev/null +++ b/contract.go @@ -0,0 +1,140 @@ +package archethic + +import ( + "bytes" + "compress/flate" + "encoding/json" + "fmt" + + "golang.org/x/exp/maps" +) + +// Contract represents a WASM smart contract +type Contract struct { + Bytecode []byte + Manifest ContractManifest +} + +// NewCompressedContract instanciates a new contract by compressing the bytecode +func NewCompressedContract(bytecode []byte, manifest ContractManifest) Contract { + var b bytes.Buffer + w, err := flate.NewWriter(&b, -1) + if err != nil { + panic(fmt.Sprintf("Cannot compress the bytecode %s", err.Error())) + } + + w.Write(bytecode) + w.Close() + + return Contract{ + Bytecode: b.Bytes(), + Manifest: manifest, + } +} + +func (c Contract) toBytes() []byte { + buf := []byte{1} + buf = append(buf, EncodeInt32(uint32(len(c.Bytecode)))...) + buf = append(buf, c.Bytecode...) + buf = append(buf, c.Manifest.toBytes()...) + + return buf +} + +// Actions returns the action of a contract +func (c Contract) Actions() []ContractAction { + actions := make([]ContractAction, 0) + for name, f := range c.Manifest.ABI.Functions { + if f.FunctionType == Action && f.TriggerType == TriggerTransaction { + actions = append(actions, ContractAction{ + Name: name, + Parameters: maps.Keys(f.Input), + }) + } + } + + return actions +} + +// ContractAction represents an overview of the contract's action +type ContractAction struct { + Name string + Parameters []string +} + +// ContractManifest represents a manifest or specification of the contract used by clients & third-party +type ContractManifest struct { + ABI WasmABI `json:"abi"` + UpgradeOpts `json:"upgradeOpts"` +} + +func (m ContractManifest) toBytes() []byte { + var iface map[string]interface{} + + bytes, _ := json.Marshal(m) + json.Unmarshal(bytes, &iface) + bytes, err := SerializeTypedData(iface) + if err != nil { + panic("invalid manifest") + } + return bytes +} + +// WasmABI represents the interface to communicate with the WASM binary defining functions and state types +type WasmABI struct { + State map[string]string `json:"state"` + Functions map[string]WasmFunctionABI `json:"functions"` +} + +// WasmFunctionABI represent the specification and the interface of a function +type WasmFunctionABI struct { + FunctionType WasmFunctionType `json:"type"` + TriggerType WasmTriggerType `json:"triggerType"` + TriggerArgument string `json:"triggerArgument,omitempty"` + Input map[string]interface{} `json:"input"` + Output map[string]interface{} `json:"output"` +} + +func (f WasmFunctionABI) toMap(name string) map[string]interface{} { + return map[string]interface{}{ + "name": name, + "type": f.FunctionType, + "triggerType": f.TriggerType, + "triggerArgument": f.TriggerArgument, + "input": f.Input, + "output": f.Output, + } +} + +// WasmFunctionType represents the type of function +type WasmFunctionType string + +// WasmTrigger represents the type of a trigger to execute the function +type WasmTriggerType string + +const ( + // Action represents a function triggered by a transaction + Action WasmFunctionType = "action" + + // PublicFunction represents a function called by anyone (readonly) + PublicFunction WasmFunctionType = "publicFunction" +) + +const ( + // TriggerTransaction represents an action triggered by a transaction + TriggerTransaction WasmTriggerType = "transaction" + + // TriggerDateTime represents an action triggered by a datetime timestamp + TriggerDateTime WasmTriggerType = "datetime" + + // TriggerInterval represents an action triggered by a cron interval + TriggerInterval WasmTriggerType = "interval" + + // TriggerOracle represents an action triggered by an oracle's event + TriggerOracle WasmTriggerType = "oracle" +) + +// UpgradeOpts represents the options to allow the upgrade of the contract +type UpgradeOpts struct { + From string `json:"from"` +} diff --git a/go.mod b/go.mod index bf00f45..7212fe5 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/archethic-foundation/libgo -go 1.20 +go 1.22.0 + +toolchain go1.22.4 require ( github.com/aead/ecdh v0.2.0 @@ -9,8 +11,8 @@ require ( github.com/decred/dcrd/dcrec/secp256k1 v1.0.3 github.com/hasura/go-graphql-client v0.9.1 github.com/nshafer/phx v0.2.0 - golang.org/x/crypto v0.6.0 github.com/ybbus/jsonrpc/v3 v3.1.4 + golang.org/x/crypto v0.6.0 ) require ( @@ -19,6 +21,7 @@ require ( github.com/google/uuid v1.3.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/klauspost/compress v1.16.0 // indirect + golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 golang.org/x/sys v0.5.0 // indirect nhooyr.io/websocket v1.8.7 // indirect ) diff --git a/go.sum b/go.sum index 4a23996..e7ecca2 100644 --- a/go.sum +++ b/go.sum @@ -68,6 +68,8 @@ golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= +golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/transaction_builder.go b/transaction_builder.go index 2e14c65..7173e5e 100644 --- a/transaction_builder.go +++ b/transaction_builder.go @@ -14,7 +14,7 @@ import ( type TransactionType uint8 const ( - Version uint32 = 3 + Version uint32 = 4 KeychainType TransactionType = 255 KeychainAccessType TransactionType = 254 TransferType TransactionType = 253 @@ -38,7 +38,7 @@ type TransactionBuilder struct { type TransactionData struct { Content []byte - Code []byte + Contract *Contract Ledger Ledger Ownerships []Ownership Recipients []Recipient @@ -47,8 +47,12 @@ type TransactionData struct { func (t TransactionData) toBytes(tx_version uint32) []byte { buf := make([]byte, 0) - // Encode code - buf = appendSizeAndContent(buf, t.Code, 32) + // Encode contract + if t.Contract == nil { + buf = append(buf, byte(0)) + } else { + buf = append(buf, t.Contract.toBytes()...) + } // Encode content buf = appendSizeAndContent(buf, t.Content, 32) @@ -247,8 +251,8 @@ func NewTransaction(txType TransactionType) *TransactionBuilder { Version: Version, TxType: txType, Data: TransactionData{ - Code: []byte{}, - Content: []byte{}, + Contract: nil, + Content: []byte{}, Ledger: Ledger{ Uco: UcoLedger{ Transfers: []UcoTransfer{}, @@ -267,8 +271,8 @@ func (t *TransactionBuilder) SetContent(content []byte) { t.Data.Content = content } -func (t *TransactionBuilder) SetCode(code string) { - t.Data.Code = []byte(code) +func (t *TransactionBuilder) SetContract(contract Contract) { + t.Data.Contract = &contract } func (t *TransactionBuilder) SetType(txType TransactionType) { @@ -469,9 +473,29 @@ func (t *TransactionBuilder) ToJSONMap() (map[string]interface{}, error) { "args": args, } } + + var contract map[string]interface{} = nil + + if t.Data.Contract != nil { + functionsWASMABI := make([]map[string]interface{}, 0) + for fun, f := range t.Data.Contract.Manifest.ABI.Functions { + functionsWASMABI = append(functionsWASMABI, f.toMap(fun)) + } + + contract = map[string]interface{}{ + "bytecode": hex.EncodeToString(t.Data.Contract.Bytecode), + "manifest": map[string]interface{}{ + "abi": map[string]interface{}{ + "functions": functionsWASMABI, + "state": t.Data.Contract.Manifest.ABI.State, + }, + }, + } + } + data := map[string]interface{}{ "content": string(t.Data.Content), - "code": string(t.Data.Code), + "contract": contract, "ownerships": ownerships, "ledger": map[string]interface{}{ "uco": map[string]interface{}{ diff --git a/transaction_builder_test.go b/transaction_builder_test.go index a993790..1381c5a 100644 --- a/transaction_builder_test.go +++ b/transaction_builder_test.go @@ -2,6 +2,7 @@ package archethic import ( "encoding/hex" + "encoding/json" "reflect" "testing" ) @@ -50,14 +51,26 @@ func TestSetType(t *testing.T) { } } -func TestTransactionBuilder_SetCode(t *testing.T) { +func TestTransactionBuilder_SetContract(t *testing.T) { tx := TransactionBuilder{ TxType: TransferType, } - tx.SetCode("my smart contract code") // "my smart contract code" in hex - if !reflect.DeepEqual(string(tx.Data.Code), "my smart contract code") { - t.Errorf("Failed to set transaction code") + contract := Contract{ + Bytecode: []byte{1, 2, 3, 4}, + Manifest: ContractManifest{ + ABI: WasmABI{ + Functions: map[string]WasmFunctionABI{ + "a": {FunctionType: Action, TriggerType: TriggerTransaction}, + }, + }, + }, + } + + tx.SetContract(contract) + + if !reflect.DeepEqual(tx.Data.Contract, &contract) { + t.Errorf("Failed to set transaction contract") } } @@ -156,16 +169,6 @@ func TestAddTokenTransfer(t *testing.T) { } func TestPreviousSignaturePayload(t *testing.T) { - code := ` - condition inherit: [ - uco_transferred: 0.020 - ] - - actions triggered by: transaction do - set_type transfer - add_uco_ledger to: "000056E763190B28B4CF9AAF3324CF379F27DE9EF7850209FB59AA002D71BA09788A", amount: 0.020 - end - ` content := []byte("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sit amet leo egestas, lobortis lectus a, dignissim orci.") secret := []byte("mysecret") @@ -184,6 +187,17 @@ func TestPreviousSignaturePayload(t *testing.T) { }, ) + contract := Contract{ + Bytecode: []byte{1, 2, 3, 4}, + Manifest: ContractManifest{ + ABI: WasmABI{ + Functions: map[string]WasmFunctionABI{ + "a": {FunctionType: Action, TriggerType: TriggerTransaction}, + }, + }, + }, + } + amount, _ := ParseBigInt("0.202", 8) amount2, _ := ParseBigInt("100", 8) tx.AddUcoTransfer( @@ -194,7 +208,7 @@ func TestPreviousSignaturePayload(t *testing.T) { []byte("0000501fa2db78bcf8ceca129e6139d7e38bf0d61eb905441056b9ebe6f1d1feaf88"), amount2, 1) - tx.SetCode(code) + tx.SetContract(contract) tx.SetContent(content) // a unnamed action @@ -231,13 +245,20 @@ func TestPreviousSignaturePayload(t *testing.T) { expectedBinary := make([]byte, 0) // Version - expectedBinary = append(expectedBinary, EncodeInt32(3)...) + expectedBinary = append(expectedBinary, EncodeInt32(4)...) expectedBinary = append(expectedBinary, tx.Address...) expectedBinary = append(expectedBinary, []byte{253}...) - // Code size - expectedBinary = append(expectedBinary, EncodeInt32(uint32(len(code)))...) - expectedBinary = append(expectedBinary, []byte(code)...) + // Contract size + var iface map[string]interface{} + bytes, _ := json.Marshal(contract.Manifest) + json.Unmarshal(bytes, &iface) + manifestBytes, _ := SerializeTypedData(iface) + + expectedBinary = append(expectedBinary, byte(1)) + expectedBinary = append(expectedBinary, EncodeInt32(uint32(len(contract.Bytecode)))...) + expectedBinary = append(expectedBinary, contract.Bytecode...) + expectedBinary = append(expectedBinary, manifestBytes...) // Content size expectedBinary = append(expectedBinary, EncodeInt32(uint32(len(content)))...) @@ -406,16 +427,6 @@ func TestBuild(t *testing.T) { } func TestOriginSignaturePayload(t *testing.T) { - code := ` - condition inherit: [ - uco_transferred: 0.020 - ] - - actions triggered by: transaction do - set_type transfer - add_uco_ledger to: "000056E763190B28B4CF9AAF3324CF379F27DE9EF7850209FB59AA002D71BA09788A", amount: 0.020 - end - ` content := []byte("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sit amet leo egestas, lobortis lectus a, dignissim orci.") secret := []byte("mysecret") seed := []byte("seed") @@ -445,7 +456,6 @@ func TestOriginSignaturePayload(t *testing.T) { []byte("0000501fa2db78bcf8ceca129e6139d7e38bf0d61eb905441056b9ebe6f1d1feaf88"), amount2, 1) - tx.SetCode(code) tx.SetContent(content) tx.AddRecipient( []byte("0000501fa2db78bcf8ceca129e6139d7e38bf0d61eb905441056b9ebe6f1d1feaf88")) @@ -459,13 +469,12 @@ func TestOriginSignaturePayload(t *testing.T) { expectedBinary := make([]byte, 0) // Version - expectedBinary = append(expectedBinary, EncodeInt32(3)...) + expectedBinary = append(expectedBinary, EncodeInt32(4)...) expectedBinary = append(expectedBinary, tx.Address...) expectedBinary = append(expectedBinary, []byte{253}...) - // Code size - expectedBinary = append(expectedBinary, EncodeInt32(uint32(len(code)))...) - expectedBinary = append(expectedBinary, []byte(code)...) + // Contract size + expectedBinary = append(expectedBinary, byte(0)) // Content size expectedBinary = append(expectedBinary, EncodeInt32(uint32(len(content)))...) @@ -549,13 +558,22 @@ func TestOriginSign(t *testing.T) { func TestToJSONMap(t *testing.T) { addressHex := "00002223bbd4ec3d64ae597696c7d7ade1cee65c639d885450ad2d7b75592ac76afa" address, _ := hex.DecodeString(addressHex) - code := "@version 1\ncondition inherit: []" content := "hello" + contract := Contract{ + Bytecode: []byte{1, 2, 3, 4}, + Manifest: ContractManifest{ + ABI: WasmABI{ + Functions: map[string]WasmFunctionABI{ + "a": {FunctionType: Action, TriggerType: TriggerTransaction}, + }, + }, + }, + } // prepare tx := NewTransaction(DataType) tx.SetAddress([]byte(address)) - tx.SetCode(code) + tx.SetContract(contract) tx.SetContent([]byte(content)) tx.AddRecipient(address) tx.AddRecipientWithNamedAction(address, []byte("vote_for_class_president"), []interface{}{"Rudy"}) @@ -579,9 +597,20 @@ func TestToJSONMap(t *testing.T) { } data := jsonMap["data"].(map[string]interface{}) - if data["code"] != code { - t.Error("Unexpected code") + + mapContract := data["contract"].(map[string]interface{}) + if mapContract["bytecode"] != hex.EncodeToString(contract.Bytecode) { + t.Error("Unexpected bytecode") + } + + mapManifest := mapContract["manifest"].(map[string]interface{}) + mapABI := mapManifest["abi"].(map[string]interface{}) + mapFunctions := mapABI["functions"].([]map[string]interface{}) + functionA := mapFunctions[0] + if functionA["type"] != Action { + t.Error("Unexpected function type") } + if data["content"] != content { t.Error("Unexpected content") }