-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add codec for BNS transactions #56
Conversation
Hmmm... I was trying to implement parsing and realized this is impossible as specified. When we create an
However, when we parse a transaction, we would only be able to receive the chain-dependent address of the recipient, and not the public key from which it was derived. Since Transaction is designed for the API interface we shouldn't force them to encode this if they only have the public key. I think this API change should go into another PR, but one thought was to change it to:
This would allow parsing code to return a proper tx and we can differentiate between the two in runtime (one a Uint8Array, the other an object). I'm not sure if this is going to seep out and lead to lots of uncertainty in the type system, or if there are other negative effects. Thoughts? |
Before you think I am a coding monster... It's called codegen ;)
That's all from protobufjs... only 2k from me |
I think instead of reusing the type, it would be better to create two different types. If I understood this correctly, we would have:
|
packages/bns-codec/tests/testdata.ts
Outdated
return out; | ||
} | ||
|
||
export function fromHex(hexstring: string): Uint8Array { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please reuse #57 after merge
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gladly
Actually I based the entire design that we always get a public key for the recipient from the BNS. As I am working, I see three flaws.
I think using Addresses instead of PublicKeyBundle for recipient is actually better. Whether we support PublicKeyBundle at all for recipients depends on outcome of BNS web4 key lookup design sessions. |
8440f36
to
685532b
Compare
The BNS will serve the following: address:pubkey pairs for the BNS specific coin path, that can be safely reused on any compatible chain and any chain implementing bip44
Addresses should be used for recipient, as nice as publicKey . In the case of escrow, we can use any address we want, all that needs to be the same between transactions is the publickey and the associated signature |
c5e411c
to
ecc524a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First set of comments
packages/bns-codec/package.json
Outdated
"test": "yarn build && yarn test-node", | ||
"prebuild": "yarn lint && yarn format", | ||
"build": "tsc && cp ./src/codec.* ./build/src", | ||
"findProto": "find $GOPATH/src/github.com/confio/weave $GOPATH/src/github.com/iov-one/bcp-demo -name '*.proto' -not -path '*/vendor/*' -not -path '*/examples/*'", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We use dash separated yarn subcommands usually
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks. i knew something looked funny
packages/bns-codec/src/util.ts
Outdated
|
||
const map = Array.prototype.map; | ||
const toNums = (str: string) => map.call(str, (x: string) => x.charCodeAt(0)); | ||
export const stringToArray = (str: string) => Uint8Array.from(toNums(str)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method is supposed to be called "asciiEncode", right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it should have ascii in the name.
decodeAscii
I believe.... a string has an encoding. bytes are just bytes.
at least in python, I do something like "deadbeef".decode('hex').encode('base64')
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A string has no encoding. A string is usually a list of unicode codepoints. Now there are different possibilities to encode that string in a binary representation (ascii, utf-8, utf-16, …).
Python 3 (using Python 3 is important here) example:
>>> "abc"
'abc'
>>> "abc".encode('ascii')
b'abc'
>>> "abc".encode('utf16')
b'\xff\xfea\x00b\x00c\x00'
>>> "abc".decode()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute 'decode'
Python 2 has no proper differentiation between a string and its binary representation. This is why your example works in Python 2 but not Python 3.
packages/bns-codec/src/util.ts
Outdated
}; | ||
|
||
const map = Array.prototype.map; | ||
const toNums = (str: string) => map.call(str, (x: string) => x.charCodeAt(0)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can avoid the use of that extra map:
const toNums = (str: string) => str.split('').map((x: string) => x.charCodeAt(0));
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice....
packages/bns-codec/src/util.ts
Outdated
|
||
// append chainID and nonce to the raw tx bytes to prepare for signing | ||
export const appendSignBytes = (bz: Uint8Array, chainID: ChainID, nonce: Nonce) => | ||
Uint8Array.from([...bz, ...stringToArray(chainID), ...nonce.toBytesBE()]) as SignableBytes; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we change that format to
format version | chain ID length | chain ID | nonce | bz
int32 | int32 | utf8 encoded string | int? | raw data
this would avoid all kind of overlapping fields collisions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's discuss this. please open a new issue in confio/weave.
The codec demands compatibility with the blockchain implementation, but you do have a point that the blockchain implementation may need improvement. I think this should not block the PR, but we should discuss a better sign bytes format in weave, then update across the board.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
chainIDlength prefix is good idea.
format version nice for future compatibility
// tendermint hash (will be) first 20 bytes of sha256 | ||
// probably only works after 0.21, but no need to import ripemd160 now | ||
export const tendermintHash = (data: Uint8Array) => | ||
Sha256.digest(data).then((bz: Uint8Array) => bz.slice(0, 20)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can make Sha256.digest synchronous if the asynchronous interface causes pain for the caller.
sha.js is synchronous and in nodejs we could alternatively use crypto.createHash('sha256').update('alice', 'utf8').digest()
, which is synchronous as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if there is a good reason, we can keep it async.
my main thought was that encoding is a synchronous action (no IO)
with async/await it is not hard to chain promises or async calls, and it isn't really bad.
however, it forced me to change the API.
not a super strong objection, I just want to see if it causes more pain to have sync sha256, or async encoding
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can leave as async and then decide if we want to change in another PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 just wanted to offer to change it. I see no need to have sha256 asynchronous at the moment
|
||
const { fromHex, toHex } = Encoding; | ||
|
||
describe("Control", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test is obsolete now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 thanks for that code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
decodeAscii
-> encodeAscii
or encodeAsAscii
packages/bns-codec/src/util.ts
Outdated
@@ -30,6 +30,14 @@ export const appendSignBytes = (bz: Uint8Array, chainID: ChainID, nonce: Nonce) | |||
export const tendermintHash = (data: Uint8Array) => | |||
Sha256.digest(data).then((bz: Uint8Array) => bz.slice(0, 20)); | |||
|
|||
// TODO: verify prefix, make const | |||
export const HashId = decodeAscii("hash/sha256/"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not a type -> lowecase hashId
I think I am ready now. Tested everything I could think of. |
verbosity: 4, // (0|false)|1|2|(3|true)|4 | ||
listStyle: 'indent', // "flat"|"indent" | ||
activity: true, | ||
emoji: true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😃
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is giving you a 👊 when all tests passed ;)
recipient: ensure(msg.dest, "recipient") as AddressBytes, | ||
amount: decodeToken(ensure(msg.amount)), | ||
memo: msg.memo || undefined, | ||
...base, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it definitely make more sense to put this last rather than first? If base
has some extra properties somehow they can override whatever came before.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good point
}); | ||
}; | ||
|
||
export const buildMsg = (tx: UnsignedTransaction): Promise<codec.app.ITx> => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do you feel about marking all Promise
-returning functions as async
whether or not they make use of await
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i dunna, i figured unnecessary.
what does @webmaster128 think?
if you both like it, i'll adopt.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be unnecessary runtime overhead. Changing a function from synchronous to async
is a compatible change when you return a Promise. So you can switch to async later.
public static async bytesToSign(tx: UnsignedTransaction, nonce: Nonce): Promise<SignableBytes> { | ||
// we encode it without any signatures | ||
const built = await buildUnsignedTx(tx); | ||
const bz = codec.app.Tx.encode(built).finish(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does bz
stand for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bytes.
i mean, bs is also short for bytes, but doens't sound so good (B.S.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in all my years in software development, I never came across any of those two abbreviations. So I'd prefer to use a proper word.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you clearly never worked at tendermint ;)
but sure, I can use a real word....
} | ||
|
||
// we need to create a const to properly type-check the export... | ||
// export const BNSCodec: TxCodec = Codec; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's wrong with export class BNSCodec implements TxCodec { ... }
? (Or even export default
.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't implement it somehow, as implements implies an instance of the class implements it, not the static class object....
If you can make it work, just fighting tsc sometimes.
Also, it does no longer match with the async changes.
|
||
export const decodeSignature = (signature: codec.crypto.ISignature): SignatureBytes => { | ||
if (signature.ed25519) { | ||
return signature.ed25519 as SignatureBytes; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's an asymmetry here with keys: should we maybe be returning an object with algo/data here too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess so, not sure why it was defined differently. But it has been for a while.
I would do that change as another PR, but good point.
packages/bns-codec/src/util.ts
Outdated
export const hashIdentifier = async (data: Uint8Array) => | ||
Uint8Array.from([...HashId, ...(await Sha256.digest(data))]); | ||
|
||
// typescript forces us to return number on reduce, so we count how many elements match |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typescript forces us to return number on reduce
Really?!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Try it, if you can get it to reduce an Uint8Array to a bool, please show me the code.
They should have a few more T
generic types in their Uint8Array type definition....
packages/bns-codec/src/util.ts
Outdated
// and make sure it is all | ||
export const arraysEqual = (a: Uint8Array, b: Uint8Array): boolean => | ||
a.length === b.length && | ||
a.reduce((acc: number, x: number, i: number) => (x === b[i] ? acc + 1 : acc), 0) === b.length; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about
a.length === b.length && a.every((n: number, i: number): boolean => n === b[i])
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm... did you run that one through tsc?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In any case, this method needs unittests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tested the every
implementation by Will, works. I think it is way easier to understand. Here are the tests:
it("arraysEqual works", () => {
// simple equality
expect(arraysEqual(new Uint8Array([]), new Uint8Array([]))).toEqual(true);
expect(arraysEqual(new Uint8Array([0x11]), new Uint8Array([0x11]))).toEqual(true);
expect(arraysEqual(new Uint8Array([0xaa, 0x77, 0x99]), new Uint8Array([0xaa, 0x77, 0x99]))).toEqual(true);
// identity
const array1 = new Uint8Array([]);
const array2 = new Uint8Array([0x11]);
const array3 = new Uint8Array([0xaa, 0x77, 0x99]);
expect(arraysEqual(array1, array1)).toEqual(true);
expect(arraysEqual(array2, array2)).toEqual(true);
expect(arraysEqual(array3, array3)).toEqual(true);
// unequal length
expect(arraysEqual(new Uint8Array([]), new Uint8Array([0x11]))).toEqual(false);
expect(arraysEqual(new Uint8Array([0xaa, 0xbb]), new Uint8Array([0xaa]))).toEqual(false);
expect(arraysEqual(new Uint8Array([0xaa]), new Uint8Array([0xaa, 0xbb]))).toEqual(false);
// unequal data (front, middle, end)
expect(arraysEqual(new Uint8Array([0xaa, 0xbb, 0xcc]), new Uint8Array([0x00, 0xbb, 0xcc]))).toEqual(false);
expect(arraysEqual(new Uint8Array([0xaa, 0xbb, 0xcc]), new Uint8Array([0xaa, 0x00, 0xcc]))).toEqual(false);
expect(arraysEqual(new Uint8Array([0xaa, 0xbb, 0xcc]), new Uint8Array([0xaa, 0xbb, 0x00]))).toEqual(false);
});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there are a few unit tests in codec.spec.ts, but i will add these tests and update to Will's implementation.
packages/bns-codec/src/util.ts
Outdated
a.reduce((acc: number, x: number, i: number) => (x === b[i] ? acc + 1 : acc), 0) === b.length; | ||
|
||
export const isHashIdentifier = (ident: Uint8Array): boolean => | ||
arraysEqual(HashId, ident.slice(0, HashId.length)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the slice helpful here? Seems counterproductive to the length check in arraysEqual
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ident will be longer, much longer. we just need to ensure the prefix matches.
i wanted to leave the other function as generic, as a compare should exist
await verify(swapClaimTxJson); | ||
await verify(swapTimeoutTxJson); | ||
} catch (err) { | ||
expect(err).toBe(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like you want .not.toThrow
: https://jasmine.github.io/api/3.0/matchers.html#toThrow
(Although I'm not 100% how that works with await
.)
// force result into Uint8Array for tests so it passes | ||
// if buffer of correct type as well | ||
expect(encoded.length).toEqual(pubBin.length); | ||
expect(Uint8Array.from(encoded)).toEqual(pubBin); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This check makes the length check redundant.
await verify(swapClaimTxJson); | ||
await verify(swapTimeoutTxJson); | ||
} catch (err) { | ||
expect(err).toBe(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What you want to say here is
} catch (err) {
fail("Unexpected exception: " + err.message);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
much better
expect(err.toString()).toContain("RangeError"); | ||
return; | ||
} | ||
expect(false).toBeTruthy(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fail()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DIdn't know this one....
Let me sum up what is left from my point of view:
Now I just realized there are tests for arraysEqual, which looks a bit too complicated to me with all the hexing and slicing. Maybe merge our test sets. |
Okay, I fixed this up with all review comments. The question about The other change I made as tsc failed without the |
bov
binary