diff --git a/node/pkg/watchers/algorand/test_nested_inner.block.json b/node/pkg/watchers/algorand/test_nested_inner.block.json new file mode 100644 index 0000000000..528997876f --- /dev/null +++ b/node/pkg/watchers/algorand/test_nested_inner.block.json @@ -0,0 +1,867 @@ +{ + "Sig": [ + 105, + 216, + 195, + 48, + 134, + 19, + 231, + 177, + 203, + 123, + 39, + 68, + 177, + 108, + 49, + 146, + 16, + 83, + 252, + 3, + 205, + 105, + 155, + 140, + 65, + 146, + 72, + 245, + 27, + 81, + 157, + 154, + 27, + 215, + 57, + 158, + 162, + 93, + 67, + 62, + 209, + 22, + 227, + 86, + 199, + 48, + 33, + 103, + 121, + 84, + 4, + 163, + 0, + 85, + 27, + 107, + 141, + 183, + 243, + 43, + 22, + 28, + 134, + 12 + ], + "Txn": { + "Type": "appl", + "Sender": [ + 206, + 168, + 201, + 203, + 219, + 55, + 61, + 172, + 61, + 71, + 177, + 40, + 245, + 137, + 33, + 193, + 111, + 150, + 6, + 171, + 13, + 68, + 81, + 115, + 156, + 174, + 167, + 216, + 238, + 132, + 137, + 228 + ], + "Fee": 1000, + "FirstValid": 30453933, + "LastValid": 30454933, + "Note": null, + "ApplicationID": 231231217, + "OnCompletion": 0, + "ApplicationArgs": [ + "hd0aKg==", + "nhxWOOMdX90rqGU4T5RVXpX+UXgAAAAADLE83Q==", + "AAAAAAAHoSA=" + ], + "Accounts": [ + [ + 210, + 200, + 255, + 178, + 7, + 225, + 188, + 18, + 254, + 137, + 181, + 25, + 145, + 41, + 27, + 249, + 185, + 43, + 215, + 225, + 96, + 64, + 230, + 126, + 144, + 206, + 212, + 182, + 57, + 154, + 179, + 83 + ], + [ + 137, + 240, + 161, + 164, + 216, + 254, + 162, + 141, + 120, + 49, + 250, + 230, + 145, + 1, + 126, + 6, + 19, + 84, + 124, + 105, + 213, + 48, + 63, + 5, + 170, + 88, + 13, + 35, + 36, + 240, + 151, + 156 + ] + ], + "ForeignApps": [ + 86525641, + 86525623 + ], + "ForeignAssets": [ + 212942045 + ], + "BoxReferences": [ + { + "ForeignAppIdx": 0, + "Name": "nhxWOOMdX90rqGU4T5RVXpX+UXgAAAAADLE83Q==" + } + ], + "LocalStateSchema": { + "NumUint": 0, + "NumByteSlice": 0 + }, + "GlobalStateSchema": { + "NumUint": 0, + "NumByteSlice": 0 + } + }, + "EvalDelta": { + "Logs": [ + "\u0015\u001f|u\u0000\u0000\u0000\u0000\u0000\u0007\ufffd " + ], + "InnerTxns": [ + { + "Sig": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "Txn": { + "Type": "appl", + "Sender": [ + 240, + 31, + 115, + 228, + 39, + 220, + 94, + 207, + 14, + 189, + 34, + 156, + 94, + 159, + 156, + 17, + 115, + 89, + 75, + 74, + 99, + 141, + 197, + 86, + 192, + 137, + 166, + 117, + 120, + 234, + 76, + 214 + ], + "Fee": 3000, + "FirstValid": 30453933, + "LastValid": 30454933, + "Note": null, + "Group": [ + 80, + 9, + 220, + 16, + 247, + 42, + 178, + 223, + 143, + 233, + 199, + 8, + 80, + 214, + 173, + 253, + 32, + 191, + 226, + 254, + 247, + 229, + 92, + 207, + 12, + 244, + 184, + 99, + 83, + 92, + 146, + 8 + ], + "ApplicationID": 86525641, + "OnCompletion": 0, + "ApplicationArgs": [ + "bm9w" + ] + } + }, + { + "Txn": { + "Type": "axfer", + "Sender": [ + 240, + 31, + 115, + 228, + 39, + 220, + 94, + 207, + 14, + 189, + 34, + 156, + 94, + 159, + 156, + 17, + 115, + 89, + 75, + 74, + 99, + 141, + 197, + 86, + 192, + 137, + 166, + 117, + 120, + 234, + 76, + 214 + ], + "Fee": 3000, + "FirstValid": 30453933, + "LastValid": 30454933, + "Group": [ + 80, + 9, + 220, + 16, + 247, + 42, + 178, + 223, + 143, + 233, + 199, + 8, + 80, + 214, + 173, + 253, + 32, + 191, + 226, + 254, + 247, + 229, + 92, + 207, + 12, + 244, + 184, + 99, + 83, + 92, + 146, + 8 + ], + "XferAsset": 212942045, + "AssetAmount": 500000, + "AssetReceiver": [ + 137, + 240, + 161, + 164, + 216, + 254, + 162, + 141, + 120, + 49, + 250, + 230, + 145, + 1, + 126, + 6, + 19, + 84, + 124, + 105, + 213, + 48, + 63, + 5, + 170, + 88, + 13, + 35, + 36, + 240, + 151, + 156 + ] + }, + "AuthAddr": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "ClosingAmount": 0, + "AssetClosingAmount": 0, + "SenderRewards": 0, + "ReceiverRewards": 0, + "CloseRewards": 0, + "EvalDelta": { + "GlobalDelta": null, + "LocalDeltas": null, + "Logs": null, + "InnerTxns": null + }, + "ConfigAsset": 0, + "ApplicationID": 0 + }, + { + "Txn": { + "Type": "appl", + "Sender": [ + 240, + 31, + 115, + 228, + 39, + 220, + 94, + 207, + 14, + 189, + 34, + 156, + 94, + 159, + 156, + 17, + 115, + 89, + 75, + 74, + 99, + 141, + 197, + 86, + 192, + 137, + 166, + 117, + 120, + 234, + 76, + 214 + ], + "Fee": 3000, + "FirstValid": 30453933, + "LastValid": 30454933, + "Note": null, + "GenesisID": "", + "Group": [ + 80, + 9, + 220, + 16, + 247, + 42, + 178, + 223, + 143, + 233, + 199, + 8, + 80, + 214, + 173, + 253, + 32, + 191, + 226, + 254, + 247, + 229, + 92, + 207, + 12, + 244, + 184, + 99, + 83, + 92, + 146, + 8 + ], + "ApplicationID": 86525641, + "OnCompletion": 0, + "ApplicationArgs": [ + "c2VuZFRyYW5zZmVy", + "AAAAAAyxPN0=", + "AAAAAAAHoSA=", + "AAAAAAAAAAAAAAAAnhxWOOMdX90rqGU4T5RVXpX+UXg=", + "AAAAAAAAAAU=", + "AAAAAAAAAAA=" + ], + "Accounts": [ + [ + 210, + 200, + 255, + 178, + 7, + 225, + 188, + 18, + 254, + 137, + 181, + 25, + 145, + 41, + 27, + 249, + 185, + 43, + 215, + 225, + 96, + 64, + 230, + 126, + 144, + 206, + 212, + 182, + 57, + 154, + 179, + 83 + ], + [ + 137, + 240, + 161, + 164, + 216, + 254, + 162, + 141, + 120, + 49, + 250, + 230, + 145, + 1, + 126, + 6, + 19, + 84, + 124, + 105, + 213, + 48, + 63, + 5, + 170, + 88, + 13, + 35, + 36, + 240, + 151, + 156 + ], + [ + 137, + 240, + 161, + 164, + 216, + 254, + 162, + 141, + 120, + 49, + 250, + 230, + 145, + 1, + 126, + 6, + 19, + 84, + 124, + 105, + 213, + 48, + 63, + 5, + 170, + 88, + 13, + 35, + 36, + 240, + 151, + 156 + ] + ], + "ForeignApps": [ + 86525623 + ], + "ForeignAssets": [ + 212942045 + ], + "BoxReferences": null, + "LocalStateSchema": { + "NumUint": 0, + "NumByteSlice": 0 + }, + "GlobalStateSchema": { + "NumUint": 0, + "NumByteSlice": 0 + } + }, + "EvalDelta": { + "GlobalDelta": null, + "LocalDeltas": null, + "Logs": null, + "InnerTxns": [ + { + "Txn": { + "Type": "appl", + "Sender": [ + 98, + 65, + 255, + 220, + 3, + 43, + 105, + 59, + 251, + 133, + 68, + 133, + 143, + 4, + 3, + 222, + 200, + 111, + 46, + 23, + 32, + 175, + 159, + 52, + 248, + 214, + 95, + 229, + 116, + 182, + 35, + 140 + ], + "Fee": 0, + "FirstValid": 30453933, + "LastValid": 30454933, + "Note": "cHVibGlzaE1lc3NhZ2U=", + "AssetFrozen": false, + "ApplicationID": 86525623, + "OnCompletion": 0, + "ApplicationArgs": [ + "cHVibGlzaE1lc3NhZ2U=", + "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB6EgAAAAAAAAAAAAAAAAnDySg9PkSFRpfNItP6okDPsDKIkABQAAAAAAAAAAAAAAAJ4cVjjjHV/dK6hlOE+UVV6V/lF4AAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==", + "AAAAAAAAAAA=" + ], + "Accounts": [ + [ + 210, + 200, + 255, + 178, + 7, + 225, + 188, + 18, + 254, + 137, + 181, + 25, + 145, + 41, + 27, + 249, + 185, + 43, + 215, + 225, + 96, + 64, + 230, + 126, + 144, + 206, + 212, + 182, + 57, + 154, + 179, + 83 + ] + ], + "ForeignApps": null, + "ForeignAssets": null, + "BoxReferences": null, + "LocalStateSchema": { + "NumUint": 0, + "NumByteSlice": 0 + }, + "GlobalStateSchema": { + "NumUint": 0, + "NumByteSlice": 0 + }, + "ApprovalProgram": null, + "ClearStateProgram": null, + "ExtraProgramPages": 0, + "StateProofType": 0, + "StateProof": null, + "Message": { + "BlockHeadersCommitment": null, + "VotersCommitment": null, + "LnProvenWeight": 0, + "FirstAttestedRound": 0, + "LastAttestedRound": 0 + } + }, + "EvalDelta": { + "GlobalDelta": null, + "LocalDeltas": { + "1": { + "\u0000": { + "Action": 1, + "Bytes": "\u0000\u0000\u0000\u0000\u0000\u0000\u0003\ufffd\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", + "Uint": 0 + } + } + }, + "Logs": [ + "AAAAAAAAA+E=" + ], + "InnerTxns": null + }, + "ConfigAsset": 0, + "ApplicationID": 0 + } + ] + }, + "ConfigAsset": 0, + "ApplicationID": 0 + } + ] + }, + "HasGenesisID": true, + "HasGenesisHash": false +} \ No newline at end of file diff --git a/node/pkg/watchers/algorand/watcher.go b/node/pkg/watchers/algorand/watcher.go index e72e893a93..aac42b067b 100644 --- a/node/pkg/watchers/algorand/watcher.go +++ b/node/pkg/watchers/algorand/watcher.go @@ -24,6 +24,9 @@ import ( "go.uber.org/zap" ) +// Algorand allows max depth of 8 inner transactions +const MAX_DEPTH = 8 + type ( // Watcher is responsible for looking over Algorand blockchain and reporting new transactions to the appid Watcher struct { @@ -39,6 +42,13 @@ type ( next_round uint64 } + + algorandObservation struct { + emitterAddress vaa.Address + nonce uint32 + sequence uint64 + payload []byte + } ) var ( @@ -77,31 +87,60 @@ func NewWatcher( } } -func lookAtTxn(e *Watcher, t types.SignedTxnInBlock, b types.Block, logger *zap.Logger) { - for q := 0; q < len(t.EvalDelta.InnerTxns); q++ { - var it = t.EvalDelta.InnerTxns[q] - var at = it.Txn +// gatherObservations recurses through a given transactions inner-transactions +// to find any messages emitted from the core wormhole contract. +// Algorand allows up to 8 levels of inner transactions. +func gatherObservations(e *Watcher, t types.SignedTxnWithAD, depth int, logger *zap.Logger) (obs []algorandObservation) { - if (len(at.ApplicationArgs) != 3) || (uint64(at.ApplicationID) != e.appid) { - continue - } + // SECURITY defense-in-depth: don't recurse > max depth allowed by Algorand + if depth >= MAX_DEPTH { + logger.Error("algod client", zap.Error(fmt.Errorf("exceeded max depth of %d", MAX_DEPTH))) + return + } - if string(at.ApplicationArgs[0]) != "publishMessage" { - continue - } + // recurse through nested inner transactions + for _, itxn := range t.EvalDelta.InnerTxns { + obs = append(obs, gatherObservations(e, itxn, depth+1, logger)...) + } - var ed = it.EvalDelta - if len(ed.Logs) == 0 { - continue - } + var at = t.Txn + var ed = t.EvalDelta - emitter := at.Sender + // check if the current transaction meets what we expect + // for an emitted message + if (len(at.ApplicationArgs) != 3) || (uint64(at.ApplicationID) != e.appid) || string(at.ApplicationArgs[0]) != "publishMessage" || len(ed.Logs) == 0 { + return + } - var a vaa.Address - copy(a[:], emitter[:]) // 32 bytes = 8edf5b0e108c3a1a0a4b704cc89591f2ad8d50df24e991567e640ed720a94be2 + logger.Info("emitter: " + hex.EncodeToString(at.Sender[:])) - logger.Info("emitter: " + hex.EncodeToString(emitter[:])) + var a vaa.Address + copy(a[:], at.Sender[:]) // 32 bytes = 8edf5b0e108c3a1a0a4b704cc89591f2ad8d50df24e991567e640ed720a94be2 + obs = append(obs, algorandObservation{ + nonce: uint32(binary.BigEndian.Uint64(at.ApplicationArgs[2])), + sequence: binary.BigEndian.Uint64([]byte(ed.Logs[0])), + emitterAddress: a, + payload: at.ApplicationArgs[1], + }) + + return +} + +// lookAtTxn takes an outer transaction from the block.payset and gathers +// observations from messages emitted in nested inner transactions +// then passes them on the relevant channels +func lookAtTxn(e *Watcher, t types.SignedTxnInBlock, b types.Block, logger *zap.Logger) { + + observations := gatherObservations(e, t.SignedTxnWithAD, 0, logger) + + // We use the outermost transaction id in the observation message + // so we can apply the same logic to gather any messages emitted + // by inner transactions + var txHash eth_common.Hash + if len(observations) > 0 { + // Repopulate the genesis id/hash for the transaction + // since in the block encoding, it's omitted to save space t.Txn.GenesisID = b.GenesisID t.Txn.GenesisHash = b.GenesisHash Id := crypto.GetTxID(t.Txn) @@ -109,21 +148,22 @@ func lookAtTxn(e *Watcher, t types.SignedTxnInBlock, b types.Block, logger *zap. id, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(Id) if err != nil { logger.Error("Base32 DecodeString", zap.Error(err)) - continue + return } - logger.Info("id: " + hex.EncodeToString(id) + " " + Id) - var txHash = eth_common.BytesToHash(id) // 32 bytes = d3b136a6a182a40554b2fafbc8d12a7a22737c10c81e33b33d1dcb74c532708b + txHash = eth_common.BytesToHash(id) // 32 bytes = d3b136a6a182a40554b2fafbc8d12a7a22737c10c81e33b33d1dcb74c532708b + } + for _, obs := range observations { observation := &common.MessagePublication{ TxHash: txHash, Timestamp: time.Unix(b.TimeStamp, 0), - Nonce: uint32(binary.BigEndian.Uint64(at.ApplicationArgs[2])), - Sequence: binary.BigEndian.Uint64([]byte(ed.Logs[0])), + Nonce: obs.nonce, + Sequence: obs.sequence, EmitterChain: vaa.ChainIDAlgorand, - EmitterAddress: a, - Payload: at.ApplicationArgs[1], + EmitterAddress: obs.emitterAddress, + Payload: obs.payload, ConsistencyLevel: 0, } diff --git a/node/pkg/watchers/algorand/watcher_test.go b/node/pkg/watchers/algorand/watcher_test.go new file mode 100644 index 0000000000..4fbbea5e43 --- /dev/null +++ b/node/pkg/watchers/algorand/watcher_test.go @@ -0,0 +1,62 @@ +package algorand + +import ( + "encoding/base64" + "encoding/json" + "os" + "testing" + + "github.com/algorand/go-algorand-sdk/types" + "github.com/certusone/wormhole/node/pkg/common" + gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" + "go.uber.org/zap" +) + +const APP_ID = 86525623 + +// Tests for nested inner transactions calling the core bridge +func TestLookAtTxnInnerTxn(t *testing.T) { + // Setup a watcher + msgC := make(chan *common.MessagePublication) + obsvReqC := make(chan *gossipv1.ObservationRequest, 50) + w := NewWatcher("", "", "", "", APP_ID, msgC, obsvReqC) + + var expectedSequence uint64 = 993 + + // read in test block for inner transactions + b, err := os.ReadFile("test_nested_inner.block.json") + if err != nil { + t.Fatalf("failed to read block file: %s", err) + } + + txn := types.SignedTxnInBlock{} + err = json.Unmarshal(b, &txn) + if err != nil { + t.Fatalf("failed to unmarshal block: %s", err) + } + + // Because we are using a json blob and the type of logs array is []string + // and because go json package will refuse to properly encode/decode + // invalid utf8 characters, the json blob has the relevant log encoded as base64 + // and we base64 decode it and convert it to a string _manually_ so we can + // make sure we got the right sequence number + b64Data := txn.EvalDelta.InnerTxns[2].EvalDelta.InnerTxns[0].EvalDelta.Logs[0] + bb, err := base64.StdEncoding.DecodeString(b64Data) + if err != nil { + t.Fatalf("Cant decode: %s", err) + } + txn.EvalDelta.InnerTxns[2].EvalDelta.InnerTxns[0].EvalDelta.Logs[0] = string(bb) + + // for each tx in the block, check to see if its a valid + // wh emitted message + logger, _ := zap.NewProduction() + observations := gatherObservations(w, txn.SignedTxnWithAD, 0, logger) + + if len(observations) != 1 { + t.Fatalf("expected 1 observation, got %d", len(observations)) + } + + if observations[0].sequence != expectedSequence { + t.Fatalf("expected sequence observed to be %d, got %d", expectedSequence, observations[0].sequence) + } +}