The point I'm about to make in this comment is so old and has been said so many times that we all are tired of hearing it. But why do we use a language like C++ to implement something where we don't wish to have bugs?
C++ is not memory-safe (no GC or whatever Rust does), it's not type safe (in the ML sense), it relies on writing to memory a lot (instead of having pure functions). Had this program been written in Ocaml or Haskell or a strongly typed functional Lisp, this bug would have been impossible to make.
Why do we do this, still? Is it not irresponsible?
You are looking at a 1k LOC function in a 7k LOC file with a bug that can be easily spotted by -Wall and you think it's the fault of the programming language? The only thing irresponsible is putting your hard-earned money into the 100th Bitcoin fork maintained by some guy with an Anime avatar. Once you stop trusting the developers it essentially becomes an underhanded ... contest and no language wins.
Didn't try, but I could imagine that running -Wall over that codebase produces huge amounts of noise rendering it practically useless, until somebody gets to fix all warnings which are not bugs.
> -Wall over that codebase produces huge amounts of noise rendering it practically useless
As a c++ developer I would nope the hell out of there or spend a month fixing the warnings - depending on pay. No way would I work with something in that state long term.
> until somebody gets to fix all warnings which are not bugs.
Code that is filled with ignored warnings generally gets worse, not better. Having little to no quality standards does that.
I think that's kind of OP's point... Use a more modern language that actually treats these kind of bugs (and many many others) as errors rather than as optional warnings.
There's a reason practically no one uses a memory-safe C++ implementation.
Yep. I'd add that if it's a warning you're absolutely certain of, use the warning control pragmas to suppress them, and document why the warning's suppressed. Keep the compiler output clean so you can see where you need to examine.
Worth noting you should probably disable -Werror for distribution if you expect users to compile your code. They aren't going to fix your error for you and you can't know what warnings will be added to future compilers, nor what previous compiler versions marked as errors. Among many other people saying the same thing see [0] if you don't trust me.
Also IMO you often want to explicitly disable a few of those from -Wall and -Wextra -- Looking at the last c++ project I wrote, I turned off a few things like `-Wno-unused-parameter` for example.
As you get further into a project you tend to need subtler tools. But for the rough work when starting out, my preference is to be stricter, which can be relaxed as necessary, rather than starting out lax and reaching a point where tightening becomes Sisyphean.
Which does actually turn on all warnings (so you'll probably want to add things like -Wno-unused -Wno-padded).
Under gcc, '-Wall -Wextra -Wpedantic' does not enable all warnings. There are still more than 40 flags you have to give if you really want all of them.
Zcash, for example, is built with `-Werror` (edit: not `-Wall`, but we're working on that). So this is absolutely feasible on a Bitcoin-derived codebase.
In any trusted codebase fixing every -Wall warning is the least you should aim for, possibly with a few intentful exceptions. Not doing so is very sloppy.
What is even worse that those guys are not able to use C++ properly. They created function with more than 1000 lines with so many branches! It will be almost impossible to write test which tries all code paths. I do not think that changing language would help in this case with such programmers.
The bug happened in "inlined constructor" so they should check all uses of this class/struct to check if it is not copied to other places (and probably all other structs/classes).
Because pretty much any language that's been touted as a replacement has problems which need solved before they'd really be viable. They're either single vendor with no independent standardization, like Rust or D, or have lower performance like go, or both. Once both problems are fixed, it'll be a lot easier to start looking at alternatives. Until then, there's not a language that settles into a lot of the niches C and C++ operate in.
This case specifically, a lot of the issues that allowed this to happen were logic errors which would have been valid in most languages. A functional language would not have helped very much here.
C and C++ are still used because they work, and have huge ecosystems around them. There's a lot of tooling around them, the compilers build fast, code, and are available for pretty much every planet under the sun. This particular case, it would have helped a lot if the developers looked at the warnings of the questionable code they had made, but they seem to be of the "it compiles, ship it" type that would have let those things fly regardless of the language.
Ada has multiple implementations, is pretty safe, and produces fast code. Yet it isn't popular, so I guess there are other factors that are more important than number of compilers and speed.
languages suffer from the same network effect as social networks. You could argue that Ada is a better programming language or that Ithkuil is a better spoken language but unless other people are also using them, you'll struggle to get help or hire people (or get contributors).
Ada isn't my favourite language to code in. But if I was tasked to build something safety-critical, I would strongly consider Ada SPARK [1] among the possible tools to use.
Haskell has not even close as many libraries, sure. I don't know ZCoin but I have implemented a (simple) Bitcoin wallet. For that you need to have a good crypto library, but otherwise it's not magic. Mining as well.
I think the lesser amount of libraries and lacking the chance of fine-tuning performance (but also your risk of screwing up your performance badly) is a very low price for the provably impossibility to have a mistake like this.
I'm just thinking back on my career, which has been full of bugs of all magnitudes. If I categorise my past bugs, there are many common patterns. Famous old-timers have names, like buffer overflows or memory leaks etc etc. Just not having the possibility to make those errors is such a freeing experience.
Also what's the big problem with no independent standardisation?
Code like this, there were just as many, if not more, logic errors that wouldn't be caught as there were mechanical problems that would have been caught with a more diligent compiler. There's just not the discipline there for good code, period. Too many corners cut, too much code commented out, etc.
I think with the types of errors you mentioned, that part of it definitely is ecosystem. Look at OpenBSD, for example. They've built things like a safer malloc and fairly extensive patches to gcc that have caught a lot of errors and bugs. I think C/C++ can definitely get safer without changing that much, the ecosystem just needs to get a bit angrier about warnings, etc.
Standardization, and multiple implementations, means you can have multiple sets of compiler eyeballs looking over things. The zcoin bug, for example, emits a warning with the default settings of clang, but doesn't emit a peep with the default settings of gcc. For someone working with a lot of embedded code, it also means that there's a better chance that there's an implementation of the language ready to go when you need to move to a different platform.
This bug had nothing to do with c++ and its ergonomics, let alone it's safety.
> Unfortunately when populating the denomination member variable, an equality operator was used instead of assignment, resulting in the denomination always being zero, as set in the class constructor.
That is absolutely a bug in C++'s ergonomics. Languages that use pascal-style := for assignment would not have this problem. Languages that don't have the surprising value behaviour of = would not have this problem. Hell, even as crude a language as Java would not have this problem because it doesn't allow an integer to implicitly decay to a boolean.
Building with warnings on would also have caught it, but the fact that someone started a new project without this warning turned on shows that C++'s defaults around warnings are bad, which is an ecosystem ergonomics issue.
I was using C++ in 1994. I am fully amused today to hear 'ergonomics' attributed to the language, even with the qualifier 'bad.' I'll spare you the rest of this "back in my day" post. I just can't fault the language for a project creator not including -Wall in the Makefile or whatever they used to build.
Indeed, just as in every other ML-family language since 1973. (I'm glad these things are finally getting mainstream attention, but I'm not sure why people get so excited about "Rust features" that are almost all taken straight from Ocaml)
Hey, I'm just glad to see 1970's PL research finally making it to the mainstream(ish), regardless of which language gets the marketing credit for it.
I think the main advantage Rust has over OCaml is that it is syntactically more like C. But, hey, if it's what gets HM-style type inference to the masses, I'll take it.
This doesn't have anything to do with the value behaviour of =. The statement in question was intended to be a simple assignment, throwing away the value of the = operator and only using its side-effect. Instead an equality operator was used, which produced no side-effect.
So if you want to blame anything, blame the concept of expression statements, I guess?
The idea that you form a value by creating an uninitialized one and then filling in its fields is dumb. (I suspect the field value wasn't even 0 as such but rather undefined, and the implementation happened to make it 0?). Better languages have you initialize the value directly and you can't access it until it's fully initialized. C/C++ are just really bad at expressing nontrivial values, even value literals.
(Discarding a value ought to be an error too, though that's more cumbersome to work with)
You can rightfully blame having state setting be part of your language. It is easier to program without state than with it, just like it's easier to juggle 2 balls than 3. Without state (or with the absolutely minimum of it) you can not make mistakes with it. Also, if your state-setting operator is super similar to your equality-checking operator, you can fall into this trap (like the devs in question did).
Programming without state is something that has helped me personally make my code much better. I produce less bugs by far since I stopped relying on state for so many things. A lot of micro-optimisations are not doable for me. In exchange I guess I get to run things in parallell very easily. But mostly, I get fewer bugs, which makes my life and the lives of my customers better.
Amen to that! If Rich Hickey is reading this - it is to you I owe my thanks for the many hours I have not spent on fixing state bugs. Clojure was my first functional love story.
So, just as in c++ there is a different operator of assignment and equality that requirs you to type a different thing. User error and testing failure.
Yes it is. Assignment is another word for writing state. If your programming environment doesn't include a way to set state, you will never do it. Why is doing the dangerous task of writing state so easy you just have to hit one key to do it in C++?
Let's collectively put our seat belts on, colleagues!
Even in Haskell, writing state is as easy as += or <- and this is used in nearly every actual program.
A cryptocurrency implementation like this doesn't need a language that makes state impossible. It needs static analysis, model checking, and proofs. This would be true even if it were written in your favorite functional language.
Sorry for being unclear. In my timezone it's pretty late.
If there was not state, the coder wouldn't had tried to set it. I don't think a language needs to have the `=` operator easy and close at hand. Had that not been the case, this bug (and oh so many others) would never had existed.
ZCash was built upon the Bitcoin codebase. This inherits a lot of bad decisions. Moral purity, demanding they start over again from scratch, just isn't practical.
The bug in question could have been solved had the simply compiled with minimal static analysis -- by which I mean -Wall.
C/C++ is memory safe if you turn on dynamic checking. Sure, it's twice as slow as C/C++, but still tons faster than nonsense languages like Ocaml or Haskell.
This bug had nothing to do with Zcash. Please correct your comment to say Zcoin, if that's what you meant.
In any case, Zcash is also derived from Bitcoin and builds with `-Werror` (edit: not `-Wall`, but we're working on that). That kind of minimal static analysis is certainly not sufficient to catch the majority of bugs, though.
C++ is not memory safe in any meaningful sense. There have been efforts to define a memory-safe subset, but typical large codebases, including Bitcoin, do not come close to falling within that subset.
> C/C++ is memory safe if you turn on dynamic checking.
What's the option to turn that on? Which compilers is it in?
I know there were several fat-pointer patches to GCC back in the day, but I didn't think anything remotely similar had ever gone mainstream. There's just too much existing code that relies on undefined behavior last I checked.
Yes, this bug is not about memory safety - but having the state-writing operator so easy at hand was probably what lead to the mistake in the first place. In my GP comment I went on to rant about other classes of bugs which are obsolete since the 1970s, but still happen.
Companies exist to make money so the most responsible thing to do is make as much money as possible. If breaking the law means you will make more money then do it.
See: a x b x c = x formula from Fight Club or Uber's entire business model.
Your assumption that we don't want bugs is probably why you are asking the question. Companies don't care about bugs, they care about losing money so if the bug doesn't cost money it's not worth fixing until it costs more than the fix.
I think this is actually very short sighted but that's just how the incentives align in our current system.
I make no claim to the philosophical implications of any of this.
> Why do we do this, still? Is it not irresponsible?
One reason is security - if cryptography isn't done in constant time then there is the potential for timing attacks. This is why BitCoinJ ditched BouncyCastle for their own JNI wrapper around BitCoin's libsecp256k1.
There is a rust client for Ethereum, called Parity - they do FFI calls to libsecp256k1. Similarly rust-crypto vendors most of its crypto in C that it calls through FFI. Similarly go-ethereum also uses libsecp256k1. However, I know that Tendermint uses Ed25519 instead of secp256k1, and in turn they use NaCl which does not involve FFI.
Mining for Ethereum is done on a GPU, so the miner code is necessarily written in OpenCL or CUDA.
There are a few blockchains written in functional language but they are new to the scene. There were a couple of Haskell Ethereum clients but they appear to be abandoned. I know kadena[1] and cardano[2] are written in Haskell. There is also Aeternity[3], which is the only currency I know of written in Erlang.
In the early days Charles Hoskinson apparently wanted Ethereum to be written in Scala. I don't think any of the core devs know Scala, but then again Charles Hoskinson was booted from the project so his language choice was dropped.
Synereo was to be written in Scala, but the founders (the CEO and the CTO) split in December. The CEO doesn't want Synereo to make a blockchain anymore, he wants to make some kind of escrow for ponzi schemes. I hear from the devs they want to code the thing in javascript. The CTO is making something called RChain; I heard he wants to use some pi-calculus interpreter he wrote in C90 back when he worked at Miter corporation in the 90s, but maybe he's ditching that.
From job postings I can see MaidSafe is looking for Rust programmers. MaidSafe has been in R&D for a decade, so they must have had yet another pivot recently.
To be frank, while there are functional programmers in the scene, these guys are struggling to establish themselves. There's a lot more talent in procedural programming which is why so much code is written in those languages. I'm not 100% that functional code is going to have fewer bugs which result in financial exploits.
I really hope amateurs with "a {} is a {} is a {} is a {}" level skills aren't working on cryptographic or financial software in C or C++. Write Rust, though, which is {}, and at least the box of hand grenades you're giving them doesn't have all the pins pre-pulled.
Luckily there are some Blockchain/Crypto currency startups that do understand the value of using something like Haskell. See https://iohk.io/projects/cardano/
These pump-and-dump schemes are getting ridiculous. Implementing a cryptocurrency client in Haskell isn't enough to justify creating an entirely new currency. Whether or not the protocol itself is sound is (almost) completely orthogonal to the language in which it's implemented, and at this point there are dozens of Bitcoin clients written in many languages (including Haskell [1]), so it's extremely unlikely that Bitcoin as a whole is still at risk from unsafe language choice.
Using 'unmanaged' languages for 'performance' reasons is no longer a good reason, because the likes of C# have shown multiple times that there is no reason to choose C++ over C# if you look at performance alone (difference is neglible).
Sure, it's silly benchmarks, so I wouldn't take the results as gospel. But the fact is, nobody who writes C# managed to produce a benchmark yet that beats a C++ program.
Fast languages like Rust have managed to at least match the performance of C in some benchmarks (and even beat it in others).
Properly unmanaged code will probably always be faster than managed code. But the point still stands. Code in a managed language, identify the hot spots and inject highly performant unmanaged code there. There is no need to go full C++ for 99.999% of all projects.
> nobody who writes C# managed to produce a benchmark yet that beats a C++ program
No, but I've seen Haskell programs that were much faster than the C++ program they replaced, which is what matters in the real world.
Microbenchmarks measure what happens if you have the time to polish every single line to perfection. If you have infinite developer time then C++ will be faster than most languages. But infinite developer time is a negligible use case.
It's worth pointing out that the majority of large C++ projects contain non-deterministic memory management (reference counting) and cache-inefficient object structures like C#.
Absolutely. But note that if you don't have cycles, reference counting of SOME objects is faster than having a GC for everything. The RAII idiom when used in C++ helps with statically determining that some objects can be released at defined points without having to reference count them.
Actually the type system catches that particular mistake:
error[E0308]: mismatched types
--> src/main.rs:5:6
|
5 | if x = 1 {
| ^^^^^ expected bool, found ()
|
= note: expected type `bool`
= note: found type `()`
error: aborting due to previous error
error: Could not compile `playground`.
To learn more, run the command again with --verbose.
I haven't yet reached the level of Rust mastery where I'm confident in this answer, but as far as I can tell it's very hard to make that kind of mistake. I'd be very interested to hear what others have to say on it though.
The problem isn't in the implementation, it's a problem with Zcoin itself. One should not be able to create Zcoin via software whether that's a bug or deliberate.
Sorry, that wasn't clear. With bitcoin you could create it at will, but it requires doing a certain (constantly increasing) amount of computational work. Work that can be verified. There is no way for a bug to accidentally create a bunch of bitcoin, and no way for anyone to deliberately create any without doing the work.
HN's law: all problems with C can be solved by writing a sufficiently high-brow post containing snide observations only a sophomore is smart enough to come up with..
Au contraire. I've had a long career in the field (various domains, I'm always the "code guy") and it's actually useful to me to hear people besides myself expressing the level of surprise/frustration/bemusement they experience. It keeps the trade from being utterly dry.
First of all, there is no need to be mean, even if I understand you mean well.
Isn't it true though, that with some techniques certain bugs are impossible to make? If you have not experienced it, I recommend you to seek it out and try.
Hello world in Rust is still 2.5MB if you statically compile all the libraries in since it doesn't strip unused functions. The tools and ecosystem are nowhere near what they are for C++
To be clear, you think that 829kB is the size of a statically linked C program that uses printf to print 'hello world'? In visual studio it is 25KB - 40KB. All of musl compiles to under 500KB.
A purely functional statically typed one, like OCaml or Haskell, because there would be so much less to review, and an insane amount of the reviewing work is automatic.
Zero bugs around state, zero bugs around memory mgmt, zero bugs about error conditions not being handled, zero bugs due to some data being expected but not being written, etc.
Well, a couple months back I attended a talk on a Haskell implementation of the Noise protocol.
The programmer admitted that he was a cryptography novice, and in fact a Haskell novice.
As a result the code he wrote is needlessly abstract - for one thing the guy uses Free monads and in turn ropes in template Haskell as part of his state model. I really have no idea what code he's generating.
The code has other features that make review challenging - for instance he doesn't qualify any of his imports so it's hard to tell where to look for the functions he's implementing.
Maybe you are better at auditing Haskell than I am. As DJ Bernstein writes in various places, one common exploit is to construct an elliptic curve Diffie Helman shared secret with input that isn't a curve point. I really can't tell if the guy is mitigating against this attack or not, but here you can have a look:
"We" don't. But yeah, using C++ is stupid, and anyone paying attention knows this by now. At this point the only thing is to point and laugh and raise the new generation better - I fear that, like science, programming practice will have to advance one funeral at a time.
C++ is not memory-safe (no GC or whatever Rust does), it's not type safe (in the ML sense), it relies on writing to memory a lot (instead of having pure functions). Had this program been written in Ocaml or Haskell or a strongly typed functional Lisp, this bug would have been impossible to make.
Why do we do this, still? Is it not irresponsible?