From 4743dfb008180e47d9c12ac5f137e5105f9361d8 Mon Sep 17 00:00:00 2001 From: Yukishige Nakajo Date: Tue, 7 May 2019 01:32:26 +0900 Subject: [PATCH] impl: Implement to resolve call hieralchy. tweaks: Change to warn message instead of raise error when unmatching program counter. tweaks: Disable some of noisy warnings. --- package-lock.json | 118 ++++++++++++++++++++++++++++++ package.json | 3 +- src/coverage_subprovider.js | 58 +++++++++------ src/coverager.js | 23 +++--- src/match_helper.js | 13 ++-- src/trace_collector.js | 4 +- src/tracelog_utils.js | 50 +++++++++---- src/truffle_artifacts_resolver.js | 4 +- test/match_helper.spec.js | 11 ++- test/tracelog_utils.spec.js | 20 ++--- 10 files changed, 233 insertions(+), 71 deletions(-) diff --git a/package-lock.json b/package-lock.json index 26deb32..06c8926 100644 --- a/package-lock.json +++ b/package-lock.json @@ -678,6 +678,42 @@ "to-fast-properties": "^2.0.0" } }, + "@sinonjs/commons": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz", + "integrity": "sha512-9jHK3YF/8HtJ9wCAbG+j8cD0i0+ATS9A7gXFqS36TblLPNy6rEEc+SB0imo91eCboGaBYGV/MT1/br/J+EE7Tw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/formatio": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz", + "integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^3.1.0" + } + }, + "@sinonjs/samsam": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.1.tgz", + "integrity": "sha512-wRSfmyd81swH0hA1bxJZJ57xr22kC07a1N4zuIL47yTS04bDk6AoCkczcqHEjcRPmJ+FruGJ9WBQiJwMtIElFw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.0.2", + "array-from": "^2.1.1", + "lodash": "^4.17.11" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, "abstract-leveldown": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-2.6.3.tgz", @@ -744,6 +780,12 @@ "sprintf-js": "~1.0.2" } }, + "array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", + "dev": true + }, "array-includes": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", @@ -3519,6 +3561,12 @@ "array-includes": "^3.0.3" } }, + "just-extend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", + "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==", + "dev": true + }, "keccak": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/keccak/-/keccak-1.4.0.tgz", @@ -3740,6 +3788,12 @@ "chalk": "^2.0.1" } }, + "lolex": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.0.1.tgz", + "integrity": "sha512-UHuOBZ5jjsKuzbB/gRNNW8Vg8f00Emgskdq2kvZxgBJCS0aqquAuXai/SkWORlKeZEiNQWZjFZOqIUcH9LqKCw==", + "dev": true + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -4028,6 +4082,27 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "nise": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.10.tgz", + "integrity": "sha512-sa0RRbj53dovjc7wombHmVli9ZihXbXCQ2uH3TNm03DyvOSIQbxg+pbqDKrk2oxMK1rtLGVlKxcB9rrc6X5YjA==", + "dev": true, + "requires": { + "@sinonjs/formatio": "^3.1.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "lolex": "^2.3.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "lolex": { + "version": "2.7.5", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz", + "integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==", + "dev": true + } + } + }, "node-environment-flags": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", @@ -4289,6 +4364,23 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "dev": true, + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, "path-type": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", @@ -4787,6 +4879,32 @@ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true }, + "sinon": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.3.2.tgz", + "integrity": "sha512-thErC1z64BeyGiPvF8aoSg0LEnptSaWE7YhdWWbWXgelOyThent7uKOnnEh9zBxDbKixtr5dEko+ws1sZMuFMA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.4.0", + "@sinonjs/formatio": "^3.2.1", + "@sinonjs/samsam": "^3.3.1", + "diff": "^3.5.0", + "lolex": "^4.0.1", + "nise": "^1.4.10", + "supports-color": "^5.5.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "slash": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", diff --git a/package.json b/package.json index 1c9b091..7f4fdd2 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "eslint-plugin-promise": "^3.8.0", "eslint-plugin-react": "^7.9.1", "eslint-plugin-standard": "^3.1.0", - "mocha": "^6.1.4" + "mocha": "^6.1.4", + "sinon": "^7.3.2" } } diff --git a/src/coverage_subprovider.js b/src/coverage_subprovider.js index 26697f5..245960e 100644 --- a/src/coverage_subprovider.js +++ b/src/coverage_subprovider.js @@ -1,5 +1,6 @@ const inherits = require('util').inherits const promisify = require('util').promisify +const { separateTraceLogs } = require('./tracelog_utils') const Subprovider = require('web3-provider-engine/subproviders/subprovider') const TraceCollector = require('./trace_collector') const TruffleArtifactResolver = require('./truffle_artifacts_resolver') @@ -28,7 +29,7 @@ function CoverageSubprovider(provider, jsonGlob = null) { } this.provider = provider this.resolver = jsonGlob ? new TruffleArtifactResolver(jsonGlob) : new TruffleArtifactResolver() - this.collecotr = new TraceCollector() + this.collector = new TraceCollector() } function debugTraceTransaction(txhash, cb) { @@ -37,8 +38,8 @@ function debugTraceTransaction(txhash, cb) { method: 'debug_traceTransaction', params: [txhash, { disableStorage: true, - disableMemory: true, - disableStack: true + disableMemory: true + // disableStack: true }] }, cb) } @@ -84,34 +85,42 @@ CoverageSubprovider.prototype._getCode = promisify(getCode) CoverageSubprovider.prototype.handleRequest = function(payload, next, end) { const self = this + function findContract(contractAddress) { + return async() => { + if (!self.resolver.exists(contractAddress)) { + const response = await self._getCode(contractAddress) + if (response.result != null) { self.resolver.findByCodeHash(response.result, contractAddress) } + } + } + } + function getTraceAndCollect(contractAddress) { - return function(txHash) { - self._debugTraceTransaction(txHash) - .then(res => { - self.collecotr.add(contractAddress, res.result) - return self._getCode(contractAddress) - }) - .then(getCodeResponse => { - if (!self.resolver.exists(contractAddress)) { - self.resolver.findByCodeHash(getCodeResponse.result, contractAddress) - } - }) + return async function(txHash) { + const response = await self._debugTraceTransaction(txHash) + // console.log(JSON.stringify(res)) + const separated = separateTraceLogs(response.result.structLogs) + self.collector.add(contractAddress, separated[0].traceLogs) + await findContract(contractAddress)() + for (let i = 1; i < separated.length; i++) { + const trace = separated[i] + self.collector.recordFunctionCall({ to: trace.address, data: trace.functionId }) + self.collector.add(trace.address, trace.traceLogs) + await findContract(trace.address)() + } } } - let traceFunc = function() {} + let traceFunc = async() => {} switch (payload.method) { case 'eth_call': - self.collecotr.recordFunctionCall(payload.params[0]) + self.collector.recordFunctionCall(payload.params[0]) + traceFunc = findContract(payload.params[0].to) break case 'eth_sendTransaction': const param = payload.params[0] if (!isNewContract(param.to) && param.data.length > 4) { - self.collecotr.recordFunctionCall(param) - // next(getTraceAndCollect(param.to)) - // } else { - // next(null) + self.collector.recordFunctionCall(param) traceFunc = getTraceAndCollect(param.to) } break @@ -119,9 +128,10 @@ CoverageSubprovider.prototype.handleRequest = function(payload, next, end) { this.provider.sendAsync(payload, function(err, response) { if (err) return end(err) - traceFunc(response.result) - if (response.error) return end(new Error(response.error.message)) - end(null, response.result) + traceFunc(response.result).then(() => { + if (response.error) return end(response.error.message) + end(null, response.result) + }) }) } @@ -131,6 +141,6 @@ CoverageSubprovider.prototype.start = function() { CoverageSubprovider.prototype.stop = function() { const self = this - const coverager = new Coverager(self.resolver, self.collecotr) + const coverager = new Coverager(self.resolver, self.collector) coverager.report() } diff --git a/src/coverager.js b/src/coverager.js index 4c731c3..a1a518b 100644 --- a/src/coverager.js +++ b/src/coverager.js @@ -41,26 +41,28 @@ class Coverager { report() { const self = this const matchingDatas = self._matching() - console.log('\n\n') - console.log(colors.bold.underline('Coverage report.')) + + let resultStr = '\n\n' + colors.bold.underline('Coverage report.\n') Object.keys(matchingDatas).forEach((contractName, index) => { - console.log(`(${index + 1}) ${contractName}`) + resultStr += `(${index + 1}) ${contractName}\n` const data = matchingDatas[contractName] const numInstructions = data.contract.compiler.name === 'solc' ? null : data.contract.deployedBytecode.length / 2 - this.reportUsedOpecodes(data.contract.deployedBytecode, data.traces, numInstructions) - this.reportMethodCalled(data.contract.functions, data.funcCalls) + resultStr += this.reportUsedOpecodes(data.contract.deployedBytecode, data.traces, numInstructions) + resultStr += this.reportMethodCalled(data.contract.functions, data.funcCalls) + resultStr += '\n\n' }) + console.log(resultStr) } reportUsedOpecodes(bytecodes, structLogs, numInstructions) { const matching = matchUsedOpecodes(bytecodes, structLogs, numInstructions) const usedTotally = countUsed(matching) - console.log(`deployedBytecode coverage: ${Number((usedTotally * 100) / matching.length).toFixed(2)}% (${usedTotally}/${matching.length})`) - console.log('') + return `deployedBytecode coverage: ${Number((usedTotally * 100) / matching.length).toFixed(2)}% (${usedTotally}/${matching.length})\n` } reportMethodCalled(functions, calls) { - const table = new Table({ head: ['method', 'call count'], colWidths: [20, 20], style: { head: ['bold'] } }) + const masLength = Math.max(...Object.keys(functions).map(sig => sig.length), 20) + const table = new Table({ head: ['method', 'call count'], colWidths: [masLength + 2, 20], style: { head: ['bold'] } }) let calledCount = 0 let functionCount = 0 Object.entries(matchCalledFunction(functions, calls)).forEach(item => { @@ -73,8 +75,9 @@ class Coverager { table.push(item) functionCount++ }) - console.log(`method coverage : ${Number((calledCount * 100) / functionCount).toFixed(2)}% (${calledCount}/${functionCount})`) - console.log(table.toString()) + let res = `method coverage : ${Number((calledCount * 100) / functionCount).toFixed(2)}% (${calledCount}/${functionCount})\n` + res += table.toString() + return res } } diff --git a/src/match_helper.js b/src/match_helper.js index 246ea63..5682451 100644 --- a/src/match_helper.js +++ b/src/match_helper.js @@ -19,12 +19,15 @@ function matchUsedOpecodes(bytecodes, structLogs, numInstructions) { const pc2IndexMap = getPcToInstructionIndexMapping(opcodeStructs) const res = JSON.parse(JSON.stringify(opcodeStructs)) structLogs.forEach(log => { - const sm = res[pc2IndexMap[log.pc]] - if (sm === undefined) { - throw new Error(`unknown program counter: ${log.pc}, details:\n${JSON.stringify(log, null, ' ')}`) + if (log && log.pc !== undefined) { + const sm = res[pc2IndexMap[log.pc]] + if (sm === undefined) { + console.warn(`unknown program counter: ${log.pc}, depth=${log.depth}`)//, details:\n${JSON.stringify(log, null, ' ')}`) + } else { + sm.vmTraces = sm.vmTraces || 0 + sm.vmTraces++ + } } - sm.vmTraces = sm.vmTraces || 0 - sm.vmTraces++ }) return res } diff --git a/src/trace_collector.js b/src/trace_collector.js index 497c3c4..a3beaf5 100644 --- a/src/trace_collector.js +++ b/src/trace_collector.js @@ -21,8 +21,8 @@ class TraceCollector { // deployed transaction is not function call message. return } - if (bytecodes.length < 4) { - console.warn(`Not Contract call message. ${JSON.stringify(txRequestParams)}`) + if (bytecodes.length < 8) { + // console.warn(`Not Contract call message. ${JSON.stringify(txRequestParams)}`) return } if (this.funcCallMap[address] === undefined) { diff --git a/src/tracelog_utils.js b/src/tracelog_utils.js index 7ea6b9d..0e30b42 100644 --- a/src/tracelog_utils.js +++ b/src/tracelog_utils.js @@ -40,6 +40,7 @@ function normalizedCallDatas(opcode, stack) { let gas, address, wei, inaddr, insize switch (opcode) { case OpCode.StaticCall: + case OpCode.DelegateCall: [gas, address, inaddr, insize] = reverseStack wei = 0 break @@ -51,10 +52,16 @@ function normalizedCallDatas(opcode, stack) { } function analyzeCallTarget(traceData) { - if(!isCallLike(traceData.op)) { + if (!isCallLike(traceData.op)) { throw new Error(`Not Call like opcode: ${traceData.op}`) } - const [gas, address, wei, inaddr, insize] = normalizedCallDatas(traceData.op, traceData.stack) + if (!traceData.stack) { + return { address: 'unknown', functionId: 'unknown' } + } + const [, address,, inaddr, insize] = normalizedCallDatas(traceData.op, traceData.stack) + if (!traceData.memory) { + return { address: '0x' + address.slice(24), functionId: 'unknown' } + } const flattenMemory = traceData.memory.reduce((flatten, hex) => flatten.concat(hex)) const startPos = parseInt(inaddr, 16) * 2 const endPos = startPos + parseInt(insize, 16) * 2 @@ -79,24 +86,37 @@ function separateTraceLogs(traceLogs) { traceLogs: [] } res[0] = context - traceLogs.forEach((log) => { - context.traceLogs.push(log) - if(isCallLike(log.op)) { - const analyzed = analyzeCallTarget(log) - callStack.unshift(context) // save for changing context. - context = { - address: analyzed.address, - functionId: analyzed.functionId, - traceLogs: [] + let searchFunctionId = false + traceLogs.forEach((log, index) => { + context.traceLogs.push({ depth: log.depth, op: log.op, pc: log.pc }) + if (searchFunctionId && log.op === 'CALLDATALOAD') { + const functionId = '0x' + traceLogs[index + 1].stack[0] + context.functionId = functionId + searchFunctionId = false + } + if (isCallLike(log.op)) { + if (traceLogs[index + 1] && log.depth < traceLogs[index + 1].depth) { + // if not increase depth then, send just ETH or precompiled contract. + // so, not change context. + const analyzed = analyzeCallTarget(log) + callStack.push(context) // save for changing context. + context = { + address: analyzed.address, + functionId: analyzed.functionId, + traceLogs: [] + } + if (context.functionId === 'unknown') { + searchFunctionId = true + } + res.push(context) // save separated logs. } - res.push(context) // save separated logs. - } else if(isEndOpcode(log.op)) { + } else if (isEndOpcode(log.op)) { context = callStack.pop() } }) - if(callStack.length > 0) { - throw new Error("Invalid trace logs. The pair of `Call type opcode` and `Stop type opecode` is unmatching.") + if (callStack.length > 0) { + throw new Error('Invalid trace logs. The pair of `Call type opcode` and `Stop type opecode` is unmatching.') } return res diff --git a/src/truffle_artifacts_resolver.js b/src/truffle_artifacts_resolver.js index ee36a94..2156c98 100644 --- a/src/truffle_artifacts_resolver.js +++ b/src/truffle_artifacts_resolver.js @@ -75,7 +75,7 @@ class TruffleArtifactsResolver { return } const data = conscompleteSource(artifact) - data.functions = artifact.abi.map(abiRevertFunctionSignature) + data.functions = artifact.abi.filter(item => item.type === 'function').map(abiRevertFunctionSignature) .reduce((reducer, item) => { const [key, value] = Object.entries(item)[0] reducer[value] = key @@ -122,7 +122,7 @@ class TruffleArtifactsResolver { ) }) if (index === -1) { - console.warn(`Unknown contract address: ${address}`) + // console.warn(`Unknown contract address: ${address}`) } else { this.addressIndexMap[address] = index return this.contractsData[index] diff --git a/test/match_helper.spec.js b/test/match_helper.spec.js index 7a7c425..5d98247 100644 --- a/test/match_helper.spec.js +++ b/test/match_helper.spec.js @@ -1,5 +1,6 @@ const { countUsed, matchUsedOpecodes, matchCalledFunction } = require('../src/match_helper') const { expect } = require('chai') +const sinon = require('sinon') const traceLogTemplate = { 'depth': 0, @@ -21,6 +22,12 @@ const traceLogs = (...pcs) => { describe('match_helper.js', function() { const bytecodes = '0x6001600201'// PUSH1 1 PUSH1 2 ADD + let spy + before(() => { + spy = sinon.spy(console, 'warn') + }) + after(() => spy.restore()) + describe('matchUsedOpecodes', function() { it('matchUsed', async() => { let coverage = matchUsedOpecodes(bytecodes, traceLogs(0), bytecodes.length / 2) @@ -45,8 +52,8 @@ describe('match_helper.js', function() { }) it('unknown pc', async() => { - const coverage = () => matchUsedOpecodes(bytecodes, traceLogs(0, 1), bytecodes.length / 2) - expect(coverage).to.throw(Error, 'unknown program counter: 1, details:\n') + matchUsedOpecodes(bytecodes, traceLogs(0, 1), bytecodes.length / 2) + expect(spy.calledWith('unknown program counter: 1, depth=0')).to.be.true }) }) diff --git a/test/tracelog_utils.spec.js b/test/tracelog_utils.spec.js index 30711b7..35b5715 100644 --- a/test/tracelog_utils.spec.js +++ b/test/tracelog_utils.spec.js @@ -1,5 +1,5 @@ -const {analyzeCallTarget, separateTraceLogs} = require('../src/tracelog_utils') -const {expect} = require('chai') +const { analyzeCallTarget, separateTraceLogs } = require('../src/tracelog_utils') +const { expect } = require('chai') const tracelog = { 'depth': 0, @@ -62,32 +62,32 @@ const push1TraceLog = { 'storage': null } -describe('tracelog_utils.js', function () { - describe('analyzeCallTarget', function () { - it('pure func check', async () => { +describe('tracelog_utils.js', function() { + describe('analyzeCallTarget', function() { + it('pure func check', async() => { const stackSave = [...tracelog.stack] const memorySave = [...tracelog.memory] analyzeCallTarget(tracelog) expect(tracelog.stack).to.deep.equal(stackSave) expect(tracelog.memory).to.deep.equal(memorySave) }) - it('call', async () => { + it('call', async() => { const callData = analyzeCallTarget(tracelog) expect(callData['address']).to.equal('0x345ca3e014aaf5dca488057592ee47305d9b3e10') expect(callData['functionId']).to.equal('0x75278362') }) - it('static call', async () => { + it('static call', async() => { const callData = analyzeCallTarget(staticCallTraceLog) expect(callData['address']).to.equal('0x345ca3e014aaf5dca488057592ee47305d9b3e10') expect(callData['functionId']).to.equal('0x6d4ce63c') }) - it('invalid opcodes', async () => { + it('invalid opcodes', async() => { expect(() => analyzeCallTarget(push1TraceLog)).to.throw('Not Call like opcode: PUSH1') }) }) - describe('sparateTraceLogs', function () { + describe('sparateTraceLogs', function() { const callLogs = require('./resources/multi_call_trace_logs') - it('simple', async () => { + it('simple', async() => { const callData = separateTraceLogs(callLogs.structLogs) expect(callData).have.to.lengthOf(3) expect(callData.map(data => data.address)).to.deep.equal(['base', '0x345ca3e014aaf5dca488057592ee47305d9b3e10', '0x345ca3e014aaf5dca488057592ee47305d9b3e10'])