diff --git a/.circleci/config.yml b/.circleci/config.yml index 3776c694..19f0d010 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -70,7 +70,8 @@ jobs: key: yarn-packages-{{ checksum "yarn.lock" }} paths: - ~/.cache/yarn - - run: # wait until cosm scripts have fully started (this includes time for docker pull) + - run: + name: Wait for blockchain and REST server to be ready (started in background) command: timeout 60 bash -c "until curl -s http://localhost:1317/node_info > /dev/null; do sleep 1; done" - run: environment: diff --git a/packages/sdk/src/leb128.spec.ts b/packages/sdk/src/leb128.spec.ts new file mode 100644 index 00000000..c95f8e38 --- /dev/null +++ b/packages/sdk/src/leb128.spec.ts @@ -0,0 +1,39 @@ +import { Encoding } from "@iov/encoding"; + +const { fromHex } = Encoding; + +export function leb128Encode(uint: number): Uint8Array { + if (uint < 0) throw new Error("Only non-negative values supported"); + if (uint > 0x7fffffff) throw new Error("Only values in signed int32 range allowed"); + const out = new Array(); + let value = uint; + do { + // tslint:disable: no-bitwise + let byte = value & 0b01111111; + value >>= 7; + + // more bytes to come: set high order bit of byte + if (value !== 0) byte ^= 0b10000000; + + out.push(byte); + // tslint:enable: no-bitwise + } while (value !== 0); + return new Uint8Array(out); +} + +describe("leb128", () => { + describe("leb128Encode", () => { + it("works for single byte values", () => { + // Values in 7 bit range are encoded as one byte + expect(leb128Encode(0)).toEqual(fromHex("00")); + expect(leb128Encode(20)).toEqual(fromHex("14")); + expect(leb128Encode(127)).toEqual(fromHex("7f")); + }); + + it("works for multi byte values", () => { + // from external souce (wasm-objdump) + expect(leb128Encode(145)).toEqual(fromHex("9101")); + expect(leb128Encode(1539)).toEqual(fromHex("830c")); + }); + }); +}); diff --git a/packages/sdk/src/restclient.spec.ts b/packages/sdk/src/restclient.spec.ts index a490139a..aa4cd3f9 100644 --- a/packages/sdk/src/restclient.spec.ts +++ b/packages/sdk/src/restclient.spec.ts @@ -4,10 +4,11 @@ import { Encoding } from "@iov/encoding"; import { HdPaths, Secp256k1HdWallet } from "@iov/keycontrol"; import { encodeSecp256k1Signature, makeSignBytes, marshalTx } from "./encoding"; +import { leb128Encode } from "./leb128.spec"; import { Log, parseLogs } from "./logs"; import { RestClient } from "./restclient"; import contract from "./testdata/contract.json"; -import data from "./testdata/cosmoshub.json"; +import cosmoshub from "./testdata/cosmoshub.json"; import { Coin, Msg, @@ -19,7 +20,7 @@ import { StdTx, } from "./types"; -const { fromBase64 } = Encoding; +const { fromBase64, toBase64 } = Encoding; const httpUrl = "http://localhost:1317"; const defaultNetworkId = "testing"; @@ -49,6 +50,32 @@ function makeSignedTx(firstMsg: Msg, fee: StdFee, memo: string, firstSignature: }; } +function getRandomizedContract(): Uint8Array { + const data = fromBase64(contract.data); + // The return value of the export function cosmwasm_api_0_6 is unused and + // can be randomized for testing. + // + // Find position of mutable bytes as follows: + // $ wasm-objdump -d contract.wasm | grep -F "cosmwasm_api_0_6" -A 1 + // 00e67c func[149] : + // 00e67d: 41 83 0c | i32.const 1539 + // + // In the last line, the addresses 00e67d-00e67f hold a one byte instruction + // (https://github.com/WebAssembly/design/blob/master/BinaryEncoding.md#constants-described-here) + // and a two byte value (leb128 encoded 1539) + + // Any unsigned integer from 128 to 16383 is encoded to two leb128 bytes + const min = 128; + const max = 16383; + const random = Math.floor(Math.random() * (max - min)) + min; + const bytes = leb128Encode(random); + + data[0x00e67d + 1] = bytes[0]; + data[0x00e67d + 2] = bytes[1]; + + return data; +} + describe("RestClient", () => { it("can be constructed", () => { const client = new RestClient(httpUrl); @@ -78,10 +105,9 @@ describe("RestClient", () => { describe("encodeTx", () => { it("works for cosmoshub example", async () => { pendingWithoutCosmos(); - // need to convince the compiler we have a valid string for pubkey type - const tx: StdTx = data.tx.value as StdTx; + const tx = cosmoshub.tx.value; const client = new RestClient(httpUrl); - expect(await client.encodeTx(tx)).toEqual(fromBase64(data.tx_data)); + expect(await client.encodeTx(tx)).toEqual(fromBase64(cosmoshub.tx_data)); }); }); @@ -134,6 +160,8 @@ describe("RestClient", () => { const signer = await wallet.createIdentity("abc" as ChainId, faucetPath); const client = new RestClient(httpUrl); + let codeId: number; + // upload { const memo = "My first contract on chain"; @@ -141,7 +169,7 @@ describe("RestClient", () => { type: "wasm/store-code", value: { sender: faucetAddress, - wasm_byte_code: contract.data, + wasm_byte_code: toBase64(getRandomizedContract()), source: "https://github.com/confio/cosmwasm/raw/0.7/lib/vm/testdata/contract_0.6.wasm", builder: "cosmwasm-opt:0.6.2", }, @@ -166,7 +194,10 @@ describe("RestClient", () => { expect(result.code).toBeFalsy(); const [firstLog] = parseSuccess(result.raw_log); const codeIdAttr = firstLog.events[0].attributes.find(attr => attr.key === "code_id"); - expect(codeIdAttr).toEqual({ key: "code_id", value: "1" }); + if (!codeIdAttr) throw new Error("Could not find code_id attribute"); + codeId = Number.parseInt(codeIdAttr.value, 10); + expect(codeId).toBeGreaterThanOrEqual(1); + expect(codeId).toBeLessThanOrEqual(200); } let contractAddress: string; @@ -188,7 +219,7 @@ describe("RestClient", () => { type: "wasm/instantiate", value: { sender: faucetAddress, - code_id: "1", + code_id: codeId.toString(), init_msg: { verifier: "cosmos1ltkhnmdcqemmd2tkhnx7qx66tq7e0wykw2j85k", beneficiary: "cosmos1ltkhnmdcqemmd2tkhnx7qx66tq7e0wykw2j85k", diff --git a/scripts/cosm/start.sh b/scripts/cosm/start.sh index 3722d815..2c0b285f 100755 --- a/scripts/cosm/start.sh +++ b/scripts/cosm/start.sh @@ -15,10 +15,6 @@ echo "Using temporary dir $TMP_DIR" WASMD_LOGFILE="$TMP_DIR/wasmd.log" REST_SERVER_LOGFILE="$TMP_DIR/rest-server.log" -# pull the newest copy of the docker image -# this is important as the sleep timeout below will fail on first run (downloading entire docker stack usually > 10s) -docker pull "$REPOSITORY:$VERSION" - # This starts up wasmd docker volume rm -f wasmd_data docker run --rm \ @@ -35,12 +31,10 @@ docker run --rm \ echo "wasmd running and logging into $WASMD_LOGFILE" # Debug chain start -sleep 3 -cat "$WASMD_LOGFILE" +# sleep 3 && cat "$WASMD_LOGFILE" -sleep 10 - -if [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME")" != "true" ]; then +# Use a large timeout because of potentially long image download in `docker run` +if ! timeout 120 bash -c "until docker inspect -f '{{.State.Running}}' '$CONTAINER_NAME' &> /dev/null; do sleep 0.5; done"; then echo "Container named '$CONTAINER_NAME' not running. We cannot continue." \ "This can happen when 'docker run' needs too long to download and start." \ "It might be worth retrying this step once the image is in the local docker cache." @@ -57,6 +51,10 @@ docker exec "$CONTAINER_NAME" \ echo "rest server running on http://localhost:1317 and logging into $REST_SERVER_LOGFILE" +# Give REST server some time to come alive. No idea why this helps. Needed for CI. +if [ -n "${CI:-}" ]; then + sleep 0.5 +fi + # Debug rest server start -sleep 3 -cat "$REST_SERVER_LOGFILE" +# sleep 3 && cat "$REST_SERVER_LOGFILE"